Skip to content

Commit 191a726

Browse files
Merge pull request #196 from microsoft/ve-file-upload-restrict
feat: Meaningful error notification if file upload fails due to uploading invalid file
2 parents 764429d + bd459cc commit 191a726

File tree

1 file changed

+89
-36
lines changed

1 file changed

+89
-36
lines changed

src/frontend/src/components/uploadButton.tsx

Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,24 @@
11
import React, { useCallback, useState, useEffect } from 'react';
2+
// MessageBar styles constants
3+
const messageBarErrorStyles = {
4+
root: { display: "flex", flexDirection: "column", alignItems: "left", background: "#fff4f4" },
5+
icon: { display: "none" },
6+
};
7+
8+
9+
10+
const messageBarSuccessStyles = {
11+
root: { display: "flex", alignItems: "left" },
12+
icon: { display: "none" },
13+
};
14+
15+
const messageBarWarningStyles = {
16+
root: { display: "flex", alignItems: "center" },
17+
};
18+
// Helper function to check for .sql extension
19+
const isSqlFile = (file: File): boolean => file.name.toLowerCase().endsWith('.sql');
20+
21+
// ...existing code...
222
import { useDropzone, FileRejection, DropzoneOptions } from 'react-dropzone';
323
import { CircleCheck, X } from 'lucide-react';
424
import {
@@ -40,7 +60,7 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
4060
onFileReject,
4161
onUploadStateChange,
4262
maxSize = 200 * 1024 * 1024,
43-
acceptedFileTypes = { 'application/sql': ['.sql'] },
63+
acceptedFileTypes = { 'application/sql': ['.sql'] }, // Accept only .sql files by extension
4464
selectedCurrentLanguage,
4565
selectedTargetLanguage
4666
}) => {
@@ -52,6 +72,7 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
5272
const [batchId, setBatchId] = useState<string>(uuidv4());
5373
const [allUploadsComplete, setAllUploadsComplete] = useState(false);
5474
const [fileLimitExceeded, setFileLimitExceeded] = useState(false);
75+
const [fileRejectionErrors, setFileRejectionErrors] = useState<string[]>([]);
5576
const [showFileLimitDialog, setShowFileLimitDialog] = useState(false);
5677
const navigate = useNavigate();
5778

@@ -162,34 +183,57 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
162183

163184
const onDrop = useCallback(
164185
(acceptedFiles: File[], fileRejections: FileRejection[]) => {
186+
// Use helper for .sql extension check
187+
const validFiles = acceptedFiles.filter(isSqlFile);
188+
const invalidFiles = acceptedFiles.filter(file => !isSqlFile(file));
189+
165190
// Check current files count and determine how many more can be added
166191
const remainingSlots = MAX_FILES - uploadingFiles.length;
167192

193+
if (validFiles.length > 0) {
194+
setFileRejectionErrors([]); // Clear error notification when valid file is selected
195+
}
196+
168197
if (remainingSlots <= 0) {
169-
// Already at max files, show dialog
170198
setShowFileLimitDialog(true);
171199
return;
172200
}
173201

174202
// If more files are dropped than slots available
175-
if (acceptedFiles.length > remainingSlots) {
176-
// Take only the first `remainingSlots` files
177-
const filesToUpload = acceptedFiles.slice(0, remainingSlots);
203+
if (validFiles.length > remainingSlots) {
204+
const filesToUpload = validFiles.slice(0, remainingSlots);
178205
filesToUpload.forEach(file => simulateFileUpload(file));
179-
180206
if (onFileUpload) onFileUpload(filesToUpload);
181-
182-
// Show dialog about exceeding limit
183207
setShowFileLimitDialog(true);
184208
} else {
185-
// Normal case, upload all files
186-
acceptedFiles.forEach(file => simulateFileUpload(file));
187-
if (onFileUpload) onFileUpload(acceptedFiles);
209+
validFiles.forEach(file => simulateFileUpload(file));
210+
if (onFileUpload) onFileUpload(validFiles);
188211
}
189212

190-
if (onFileReject && fileRejections.length > 0) {
213+
// Efficient error array construction
214+
const errors: string[] = [
215+
...invalidFiles.map(file =>
216+
`File '${file.name}' is not a valid SQL file. Only .sql files are allowed.`
217+
),
218+
...fileRejections.flatMap(rejection =>
219+
rejection.errors.map(err => {
220+
if (err.code === "file-too-large") {
221+
return `File '${rejection.file.name}' exceeds the 200MB size limit. Please upload a file smaller than 200MB.`;
222+
} else if (err.code === "file-invalid-type") {
223+
return `File '${rejection.file.name}' is not a valid SQL file. Only .sql files are allowed.`;
224+
} else {
225+
return `File '${rejection.file.name}': ${err.message}`;
226+
}
227+
})
228+
)
229+
];
230+
231+
if (fileRejections.length > 0 && onFileReject) {
191232
onFileReject(fileRejections);
192233
}
234+
if (errors.length > 0) {
235+
setFileRejectionErrors(errors);
236+
}
193237
},
194238
[onFileUpload, onFileReject, uploadingFiles.length]
195239
);
@@ -198,7 +242,7 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
198242
onDrop,
199243
noClick: true,
200244
maxSize,
201-
accept: acceptedFileTypes,
245+
accept: acceptedFileTypes, // Only .sql files regardless of mime type
202246
//maxFiles: MAX_FILES,
203247
};
204248

@@ -230,18 +274,19 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
230274
};
231275

232276
const cancelAllUploads = useCallback(() => {
233-
// Clear all upload intervals
234-
dispatch(deleteBatch({ batchId, headers: null }));
235-
236-
Object.values(uploadIntervals).forEach(interval => clearInterval(interval));
237-
setUploadIntervals({});
238-
setUploadingFiles([]);
239-
setUploadState('IDLE');
240-
onUploadStateChange?.('IDLE');
241-
setShowCancelDialog(false);
242-
setShowLogoCancelDialog(false);
243-
//setBatchId();
244-
startNewBatch();
277+
// Clear all upload intervals
278+
dispatch(deleteBatch({ batchId, headers: null }));
279+
280+
Object.values(uploadIntervals).forEach(interval => clearInterval(interval));
281+
setUploadIntervals({});
282+
setUploadingFiles([]);
283+
setUploadState('IDLE');
284+
onUploadStateChange?.('IDLE');
285+
setShowCancelDialog(false);
286+
setShowLogoCancelDialog(false);
287+
setFileRejectionErrors([]); // Clear error notification when cancel is clicked
288+
//setBatchId();
289+
startNewBatch();
245290
}, [uploadIntervals, onUploadStateChange]);
246291

247292
useEffect(() => {
@@ -521,15 +566,28 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
521566
</div>
522567

523568
<div style={{ display: 'flex', flexDirection: 'column', gap: '13px', width: '837px', paddingBottom: 10, borderRadius: '4px', }}>
569+
{/* Show file rejection errors for invalid type or size */}
570+
{fileRejectionErrors.length > 0 && (
571+
<MessageBar
572+
messageBarType={MessageBarType.error}
573+
isMultiline={true}
574+
styles={messageBarErrorStyles}
575+
>
576+
<div style={{ display: "flex", alignItems: "center" }}>
577+
<X strokeWidth="2.5px" color="#d83b01" size="16px" style={{ marginRight: "8px" }} />
578+
<span>{fileRejectionErrors[0]}</span>
579+
</div>
580+
{fileRejectionErrors.slice(1).map((err, idx) => (
581+
<div key={idx} style={{ marginLeft: "24px", marginTop: "2px" }}>{err}</div>
582+
))}
583+
</MessageBar>
584+
)}
524585
{/* Show network error message bar if any file has error */}
525586
{uploadingFiles.some(f => f.status === 'error') && (
526587
<MessageBar
527588
messageBarType={MessageBarType.error}
528589
isMultiline={false}
529-
styles={{
530-
root: { display: "flex", alignItems: "left", background: "#fff4f4" },
531-
icon: { display: "none" },
532-
}}
590+
styles={messageBarErrorStyles}
533591
>
534592
<div style={{ display: "flex", alignItems: "left" }}>
535593
<X
@@ -548,10 +606,7 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
548606
<MessageBar
549607
messageBarType={MessageBarType.success}
550608
isMultiline={false}
551-
styles={{
552-
root: { display: "flex", alignItems: "left" },
553-
icon: { display: "none" },
554-
}}
609+
styles={messageBarSuccessStyles}
555610
>
556611
<div style={{ display: "flex", alignItems: "left" }}>
557612
<CircleCheck
@@ -571,9 +626,7 @@ const FileUploadZone: React.FC<FileUploadZoneProps> = ({
571626
isMultiline={false}
572627
onDismiss={() => setFileLimitExceeded(false)}
573628
dismissButtonAriaLabel="Close"
574-
styles={{
575-
root: { display: "flex", alignItems: "center" },
576-
}}
629+
styles={messageBarWarningStyles}
577630
>
578631
<X
579632
strokeWidth="2.5px"

0 commit comments

Comments
 (0)