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

Allow file upload cancel #1410

Merged
merged 11 commits into from
Jul 8, 2022
2 changes: 1 addition & 1 deletion py/h2o_wave/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2907,7 +2907,7 @@ def __init__(
self.name = name
"""An identifying name for this component."""
self.label = label
"""Text to be displayed in the bottom button. Defaults to "Upload"."""
"""Text to be displayed in the bottom button or as a component title when the component is displayed compactly. Defaults to "Upload"."""
self.multiple = multiple
"""True if the component should allow multiple files to be uploaded."""
self.file_extensions = file_extensions
Expand Down
2 changes: 1 addition & 1 deletion py/h2o_wave/ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1129,7 +1129,7 @@ def file_upload(

Args:
name: An identifying name for this component.
label: Text to be displayed in the bottom button. Defaults to "Upload".
label: Text to be displayed in the bottom button or as a component title when the component is displayed compactly. Defaults to "Upload".
multiple: True if the component should allow multiple files to be uploaded.
file_extensions: List of allowed file extensions, e.g. `pdf`, `docx`, etc.
max_file_size: Maximum allowed size (Mb) per file. No limit by default.
Expand Down
2 changes: 1 addition & 1 deletion r/R/ui.R
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,7 @@ ui_mini_buttons <- function(
#' A file upload component allows a user to browse, select and upload one or more files.
#'
#' @param name An identifying name for this component.
#' @param label Text to be displayed in the bottom button. Defaults to "Upload".
#' @param label Text to be displayed in the bottom button or as a component title when the component is displayed compactly. Defaults to "Upload".
#' @param multiple True if the component should allow multiple files to be uploaded.
#' @param file_extensions List of allowed file extensions, e.g. `pdf`, `docx`, etc.
#' @param max_file_size Maximum allowed size (Mb) per file. No limit by default.
Expand Down
2 changes: 1 addition & 1 deletion ui/src/file_upload.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ describe('FileUpload.tsx', () => {
responseText: data ? JSON.stringify(data) : null,
}
// @ts-ignore
window.XMLHttpRequest = jest.fn().mockImplementation(() => xhrMockObj)
window.XMLHttpRequest = jest.fn(() => xhrMockObj)
// @ts-ignore
setTimeout(() => { xhrMockObj['onreadystatechange']() }, 0)
}
Expand Down
60 changes: 43 additions & 17 deletions ui/src/file_upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { wave } from './ui'
export interface FileUpload {
/** An identifying name for this component. */
name: Id
/** Text to be displayed in the bottom button. Defaults to "Upload". */
/** Text to be displayed in the bottom button or as a component title when the component is displayed compactly. Defaults to "Upload". */
label?: S
/** True if the component should allow multiple files to be uploaded. */
multiple?: B
Expand Down Expand Up @@ -113,13 +113,19 @@ export const
maxFileSizeBytes = max_file_size ? convertMegabytesToBytes(max_file_size) : 0,
maxSizeBytes = max_size ? convertMegabytesToBytes(max_size) : 0,
fileExtensions = file_extensions ? file_extensions.map(e => e.startsWith('.') ? e : `.${e}`) : null,
rejectXhr = React.useRef<(reason?: any) => void>(),
upload = async (uploadFiles = files) => {
const formData = new FormData()
uploadFiles.forEach((f: File) => formData.append('files', f))

try {
const { responseText } = await new Promise<XMLHttpRequest>((resolve, reject) => {
const xhr = new XMLHttpRequest()
rejectXhr.current = () => {
reject()
xhr.abort()
setPercentComplete(0)
}
xhr.open("POST", wave.uploadURL)
xhr.upload.onprogress = e => setPercentComplete(e.loaded / e.total)
xhr.send(formData)
Expand All @@ -134,8 +140,13 @@ export const
if (!compact) wave.push()
setSuccessMsg(`Successfully uploaded files: ${files.map(({ name }: File) => name).join(',')}.`)
}
catch ({ responseText }) { setError(responseText || 'There was an error when uploading file.') }
finally { setFiles([]) }
catch (err: unknown) {
if (err) setError(err instanceof XMLHttpRequest ? err.responseText : 'There was an error when uploading file.')
setFileNames('')
}
finally {
setFiles([])
}
},
isFileTypeAllowed = (fileName: S) => !fileExtensions || fileExtensions.some(ext => fileName.toLowerCase().endsWith(ext.toLowerCase())),
validateFiles = (fileArr: File[]) => {
Expand Down Expand Up @@ -173,11 +184,11 @@ export const
}
else {
setFiles(fileArr)
setFileNames(fileArr.map(({ name }) => name).join(', '))
marek-mihok marked this conversation as resolved.
Show resolved Hide resolved
if (compact) {
await upload(fileArr)
setPercentComplete(0)
}
setFileNames(fileArr.map(({ name }) => name).join(', '))
}
},
onIsDragging = (e: React.DragEvent<HTMLFormElement>) => {
Expand Down Expand Up @@ -248,12 +259,15 @@ export const
<Fluent.Text styles={{ root: { pointerEvents: 'none' } }}>Drop files anywhere within the box.</Fluent.Text>
)
else if (percentComplete) return (
<Fluent.ProgressIndicator
styles={{ root: { width: '80%' } }}
data-test='progress' // TODO: Does not work.
description={`Uploading: ${(percentComplete * 100).toFixed(2)}%`}
percentComplete={percentComplete}
/>
<>
<Fluent.ProgressIndicator
styles={{ root: { width: '80%' } }}
data-test='progress' // TODO: Does not work.
description={`Uploading: ${(percentComplete * 100).toFixed(2)}%`}
percentComplete={percentComplete}
/>
<Fluent.DefaultButton styles={{ root: { marginTop: 12 } }} text='Cancel' onClick={rejectXhr.current} />
</>
)
else if (files.length) return (
<>
Expand Down Expand Up @@ -294,17 +308,29 @@ export const
},
getCompactFileUpload = () => (
<>
{label && <Fluent.Label>{label}</Fluent.Label>}
{
percentComplete
? <Fluent.ProgressIndicator description={`Uploading: ${(percentComplete * 100).toFixed(2)}%`} percentComplete={percentComplete} />
: (
<div className={css.compact}>
<Fluent.TextField data-test={`textfield-${name}`} readOnly value={fileNames} errorMessage={error} />
<input id={name} data-test={name} type='file' hidden onChange={onChange} accept={fileExtensions?.join(',')} multiple={multiple} />
<label htmlFor={name} className={clas(css.uploadLabel, css.uploadLabelCompact)}>Browse</label>
? (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Fluent.ProgressIndicator
label={label ? <Fluent.Label>{label}</Fluent.Label> : undefined}
description={`Uploading: ${(percentComplete * 100).toFixed(2)}%`}
percentComplete={percentComplete}
styles={{ root: { flex: 1 }, itemName: { padding: 0 }, itemProgress: { paddingTop: 5, paddingBottom: 5 } }}
/>
<Fluent.IconButton iconProps={{ iconName: 'cancel' }} title='Cancel' onClick={rejectXhr.current} styles={{ root: { padding: 16, marginTop: 11, marginLeft: 6 } }} />
</div>
)
: (
<>
{label && <Fluent.Label style={{ paddingTop: 6 }}>{label}</Fluent.Label>}
<div className={css.compact}>
<Fluent.TextField data-test={`textfield-${name}`} readOnly value={fileNames} errorMessage={error} />
<input id={name} data-test={name} type='file' hidden onChange={onChange} accept={fileExtensions?.join(',')} multiple={multiple} />
<label htmlFor={name} className={clas(css.uploadLabel, css.uploadLabelCompact)}>Browse</label>
</div>
</>
)
}
</>
)
Expand Down