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

feat: add project page timeout for excel upload #3450

Merged
merged 7 commits into from
Aug 22, 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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# [1.182.0](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.181.2...v1.182.0) (2024-08-22)

### Features

- add project page timeout for excel upload ([ba9e9b5](https://github.com/bcgov/CONN-CCBC-portal/commit/ba9e9b5e5f6d40095d29ef2a860b1d7678fadf40))

## [1.181.2](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.181.1...v1.181.2) (2024-08-22)

## [1.181.1](https://github.com/bcgov/CONN-CCBC-portal/compare/v1.181.0...v1.181.1) (2024-08-22)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,109 +247,113 @@ const ProjectInformationForm: React.FC<Props> = ({
});
}

validateSow(sowFile, false).then((response) => {
const isSowErrors = sowValidationErrors.length > 0;
const isSowUploaded =
formData?.statementOfWorkUpload?.length > 0 && sowFile !== null;

// If there are sow errors, persist sow error in form data if not delete
const newFormData = { ...formData };
if (isSowErrors) {
newFormData.isSowUploadError = true;
} else if (isSowUploaded) {
delete newFormData?.isSowUploadError;
}
if (isChangeRequest) {
createChangeRequest({
variables: {
connections: [connectionId],
input: {
_applicationId: rowId,
_amendmentNumber: changeRequestAmendmentNumber,
_jsonData: newFormData,
_oldChangeRequestId: parseInt(
currentChangeRequestData?.rowId,
10
),
validateSow(sowFile, false)
.then((response) => {
const isSowErrors = sowValidationErrors.length > 0;
const isSowUploaded =
formData?.statementOfWorkUpload?.length > 0 && sowFile !== null;

// If there are sow errors, persist sow error in form data if not delete
const newFormData = { ...formData };
if (isSowErrors) {
newFormData.isSowUploadError = true;
} else if (isSowUploaded) {
delete newFormData?.isSowUploadError;
}
if (isChangeRequest) {
createChangeRequest({
variables: {
connections: [connectionId],
input: {
_applicationId: rowId,
_amendmentNumber: changeRequestAmendmentNumber,
_jsonData: newFormData,
_oldChangeRequestId: parseInt(
currentChangeRequestData?.rowId,
10
),
},
},
},
onCompleted: () => {
handleResetFormData();
onCompleted: () => {
handleResetFormData();

if (isSowUploaded && response?.status === 200) {
setShowToast(true);
}
if (isSowUploaded && response?.status === 200) {
setShowToast(true);
}

setCurrentChangeRequestData(null);
},
updater: (store) => {
// add new amendment number to the amendment numbers computed column
const applicationStore = store.get(id);

// remove amendment number if editing a change request and changing the amendment number
const newAmendmentNumbers = !isSameAmendmentNumber
? amendmentNumbers
.split(' ')
.filter(
(number) =>
number !== oldChangeRequestAmendmentNumber?.toString()
)
.join(' ')
: amendmentNumbers;

const updatedAmendmentNumbers = `${newAmendmentNumbers} ${changeRequestAmendmentNumber}`;

applicationStore.setValue(
updatedAmendmentNumbers,
'amendmentNumbers'
);

// Don't need to update store if we are creating a new change request
if (!currentChangeRequestData?.id) return;
const relayConnectionId = changeRequestDataByApplicationId.__id;
// Get the connection from the store

const connection = store.get(relayConnectionId);

store.delete(currentChangeRequestData.id);
// Remove the old announcement from the connection
ConnectionHandler.deleteNode(
connection,
currentChangeRequestData.id
);
},
onError: () => {
setIsFormSubmitting(false);
},
});
} else {
createProjectInformation({
variables: {
input: { _applicationId: rowId, _jsonData: newFormData },
},
onCompleted: () => {
handleResetFormData(!formData?.hasFundingAgreementBeenSigned);
setHasFormSaved(true);
if (isSowUploaded && response?.status === 200) {
setShowToast(true);
}
},
updater: (store, data) => {
store
.get(id)
.setLinkedRecord(
store.get(
data.createProjectInformation.projectInformationData.id
),
'projectInformation'
setCurrentChangeRequestData(null);
},
updater: (store) => {
// add new amendment number to the amendment numbers computed column
const applicationStore = store.get(id);

// remove amendment number if editing a change request and changing the amendment number
const newAmendmentNumbers = !isSameAmendmentNumber
? amendmentNumbers
.split(' ')
.filter(
(number) =>
number !== oldChangeRequestAmendmentNumber?.toString()
)
.join(' ')
: amendmentNumbers;

const updatedAmendmentNumbers = `${newAmendmentNumbers} ${changeRequestAmendmentNumber}`;

applicationStore.setValue(
updatedAmendmentNumbers,
'amendmentNumbers'
);
},
onError: () => {
setIsFormSubmitting(false);
},
});
}
});

// Don't need to update store if we are creating a new change request
if (!currentChangeRequestData?.id) return;
const relayConnectionId = changeRequestDataByApplicationId.__id;
// Get the connection from the store

const connection = store.get(relayConnectionId);

store.delete(currentChangeRequestData.id);
// Remove the old announcement from the connection
ConnectionHandler.deleteNode(
connection,
currentChangeRequestData.id
);
},
onError: () => {
setIsFormSubmitting(false);
},
});
} else {
createProjectInformation({
variables: {
input: { _applicationId: rowId, _jsonData: newFormData },
},
onCompleted: () => {
handleResetFormData(!formData?.hasFundingAgreementBeenSigned);
setHasFormSaved(true);
if (isSowUploaded && response?.status === 200) {
setShowToast(true);
}
},
updater: (store, data) => {
store
.get(id)
.setLinkedRecord(
store.get(
data.createProjectInformation.projectInformationData.id
),
'projectInformation'
);
},
onError: () => {
setIsFormSubmitting(false);
},
});
}
})
.catch(() => {
setIsFormSubmitting(false);
});
};

const isOriginalSowUpload = projectInformation?.jsonData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import styled, { keyframes } from 'styled-components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircleCheck } from '@fortawesome/free-solid-svg-icons';
import { Alert } from '@button-inc/bcgov-theme';
import parse from 'html-react-parser';

const StyledAlert = styled(Alert)`
margin-bottom: 8px;
Expand Down Expand Up @@ -94,7 +95,6 @@ export const displayExcelUploadErrors = (err) => {
error: errorMessage,
filename = 'Statement of Work',
} = err;

let title = `An unknown error has occured while validating the ${filename} data`;
if (errorType?.includes('tab')) {
title = `There was an error importing the ${filename} data at ${errorType}`;
Expand All @@ -113,6 +113,9 @@ export const displayExcelUploadErrors = (err) => {
if (errorType === 'claimNumber') {
title = `A Claim & Progress Report already exists with this claim number. Data were not imported.`;
}
if (errorType === 'timeout') {
title = `The upload of ${filename} timed out. Please try again later.`;
}
// for cell level errors
if (typeof errorMessage !== 'string') {
return errorMessage.map(({ error: message }) => {
Expand All @@ -139,7 +142,7 @@ export const displayExcelUploadErrors = (err) => {
content={
<>
<div> {title}</div>
<div>{errorMessage}</div>
<div>{parse(errorMessage)}</div>
</>
}
/>
Expand Down
53 changes: 38 additions & 15 deletions app/lib/helpers/excelValidate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Dispatch } from 'react';
import fetchWithTimeout from './fetchWithTimeout';

/**
* Function generator made to get around the "no code copying" for sonarcloud, to be used in a callback or by itself
Expand All @@ -18,23 +19,45 @@ function excelValidateGenerator(
fileFormData.append('file', file);
if (setExcelFile) setExcelFile(file);
if (setExcelValidationErrors) setExcelValidationErrors([]);
const response = await fetch(`${apiPath}/?validate=${validateOnly}`, {
method: 'POST',
body: fileFormData,
});
try {
const response = await fetchWithTimeout(
`${apiPath}/?validate=${validateOnly}`,
{
method: 'POST',
body: fileFormData,
}
);

const errorListResponse = await response.json();
if (Array.isArray(errorListResponse) && errorListResponse.length > 0) {
const errorList = errorListResponse.map((item) => {
return { ...item, filename: file.name };
});
if (setExcelValidationErrors) setExcelValidationErrors(errorList);
} else if (setExcelValidationErrors) {
setExcelValidationErrors([]);
}
const errorListResponse = await response.json();
if (Array.isArray(errorListResponse) && errorListResponse.length > 0) {
const errorList = errorListResponse.map((item) => {
return { ...item, filename: file.name };
});
if (setExcelValidationErrors) setExcelValidationErrors(errorList);
} else if (setExcelValidationErrors) {
setExcelValidationErrors([]);
}

// return error list and status since response.json has been consumed and locked
return { ...errorListResponse, status: response.status };
// return error list and status since response.json has been consumed and locked
return { ...errorListResponse, status: response.status };
} catch (error) {
if (setExcelValidationErrors) {
setExcelValidationErrors([
{
error:
' If the issue persists, <a href="mailto:meherzad.romer@gov.bc.ca">contact the development team.</a>',
level: 'timeout',
filename: file.name,
},
]);
}
return {
level: 'timeout',
error:
' If the issue persists, <a href="mailto:meherzad.romer@gov.bc.ca">contact the development team.</a>',
filename: file.name,
};
}
}
if (setExcelValidationErrors) setExcelValidationErrors([]);
return null;
Expand Down
27 changes: 27 additions & 0 deletions app/lib/helpers/fetchWithTimeout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const fetchWithTimeout = async (
url: string,
options: RequestInit = {},
// default timeout is the equivalent of the max
// liveness of a pod in OpenShift + 15 seconds
timeout: number = 105000
): Promise<Response> => {
const controller = new AbortController();
const { signal } = controller;
const fetchOptions = { ...options, signal };

const timeoutId = setTimeout(() => controller.abort(), timeout);

try {
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Fetch request to ${url} timed out after ${timeout}ms`);
}
throw error;
}
};

export default fetchWithTimeout;
4 changes: 3 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@rjsf/utils": "^5.17.1",
"@rjsf/validator-ajv8": "5.17.1",
"@sentry/nextjs": "^7.36.0",
"@smithy/node-http-handler": "^3.0.0",
"@snowplow/browser-tracker": "^3.23.0",
"@types/eslint": "^8.44.2",
"@types/formidable": "^3.4.5",
Expand All @@ -76,6 +77,7 @@
"graphql": "^15.6.1",
"graphql-upload": "^15.0.2",
"helmet": "^7.1.0",
"html-react-parser": "^5.1.12",
"iframe-resizer-react": "^1.1.0",
"js-cookie": "^3.0.5",
"json-diff": "^1.0.6",
Expand Down Expand Up @@ -103,7 +105,6 @@
"react-typography": "^0.16.23",
"relay-nextjs": "^0.8.0",
"relay-runtime": "^13.2.0",
"@smithy/node-http-handler": "^3.0.0",
"styled-components": "^5.3.5",
"typography": "^0.16.21",
"url": "^0.11.3",
Expand Down Expand Up @@ -151,6 +152,7 @@
"eslint-plugin-jest": "^28.5.0",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-relay": "^1.8.3",
"fetch-mock": "9.11.0",
"fse": "^4.0.1",
"happo-cypress": "^4.2.0",
"happo-e2e": "^2.6.0",
Expand Down
Loading
Loading