diff --git a/isimip_data/download/assets/js/api/DownloadApi.js b/isimip_data/download/assets/js/api/DownloadApi.js index 25149e3..ac303f7 100644 --- a/isimip_data/download/assets/js/api/DownloadApi.js +++ b/isimip_data/download/assets/js/api/DownloadApi.js @@ -1,3 +1,5 @@ +import { isEmpty } from 'lodash' + import { BadRequestError, downloadFile } from 'isimip_data/core/assets/js/utils/api' class DownloadApi { @@ -21,14 +23,37 @@ class DownloadApi { }) } - static submitJob(url, data) { - return fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }).then(response => { + static submitJob(url, data, uploads) { + let promise + + if (isEmpty(uploads)) { + promise = fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + } else { + const formData = new FormData() + + // append data as a JSON blob + formData.append('data', new Blob([JSON.stringify(data)]), { + type: "application/json" + }) + + // append each file + Object.entries(uploads).forEach(([fileName, file]) => { + formData.append(fileName, file) + }) + + promise = fetch(url, { + method: 'POST', + body: formData + }) + } + + return promise.then(response => { if (response.ok) { return response.json() } else { @@ -38,11 +63,6 @@ class DownloadApi { } }) } - - static downloadFile(file_url) { - - } - } export default DownloadApi diff --git a/isimip_data/download/assets/js/components/Form.js b/isimip_data/download/assets/js/components/Form.js index b45b443..df2a007 100644 --- a/isimip_data/download/assets/js/components/Form.js +++ b/isimip_data/download/assets/js/components/Form.js @@ -20,12 +20,18 @@ const Form = ({ files, setJob }) => { const handleSubmit = (event) => { event.preventDefault() + + const uploads = {} + const data = { paths, operations: operations.map(operation => { + const { file, ...values } = operation + uploads[values.mask] = file + return values + })} + mutation.mutate({ url: settings.FILES_API_URL, - data: { - paths, - operations - }, + data, + uploads, setErrors, setJob }) @@ -45,7 +51,7 @@ const Form = ({ files, setJob }) => { operations={operations} setOperations={setOperations} /> -
+
-
- { - settings && settings.DOWNLOAD_OPERATIONS.map(operation => ( - - )) - } +
+
+ +
+ { + settings && settings.DOWNLOAD_OPERATIONS.map(operation => ( + + )) + } +
+
) diff --git a/isimip_data/download/assets/js/components/form/widgets/Mask.js b/isimip_data/download/assets/js/components/form/widgets/Mask.js new file mode 100644 index 0000000..9e5d8fd --- /dev/null +++ b/isimip_data/download/assets/js/components/form/widgets/Mask.js @@ -0,0 +1,83 @@ +import React, { useState } from 'react' +import PropTypes from 'prop-types' +import { useDropzone } from 'react-dropzone' +import { isEmpty, isNil, uniqueId } from 'lodash' + +const Mask = ({ values, errors, onChange }) => { + const fileId = uniqueId('download-form-input-file-') + const maskId = uniqueId('download-form-input-mask-') + const varId = uniqueId('download-form-input-var-') + + const [dropzoneError, setDropzoneError] = useState('') + + const { getRootProps, getInputProps, isDragActive } = useDropzone({ + accept: { + 'application/x-hdf': ['.nc', '.nc4'] + }, + onDropAccepted: acceptedFiles => { + if (acceptedFiles.length > 0) { + onChange({ file: acceptedFiles[0] }) + setDropzoneError('') + } + }, + onDropRejected: rejectedFiles => { + setDropzoneError(rejectedFiles.map(file => file.errors.map(error => error.message).join()).join()) + } + }) + + return ( +
+
+
+ +
+
+ +
+ { + isDragActive ? ( +

Drop the file here ...

+ ) : ( +

Drag and drop a NetCDF mask file here, or click to select.

+ ) + } + { + !isNil(values.file) && !isNil(values.file.name) && ( +

Currently selected: {values.file.name}

+ ) + } +
+
+
+ { + dropzoneError && ( +
{dropzoneError}
+ ) + } +
+
+
+
+ + onChange({ mask: event.target.value })} /> +
+
+ + onChange({ var: event.target.value })} /> +
+
+
+ ) +} + +Mask.propTypes = { + values: PropTypes.object, + errors: PropTypes.array, + onChange: PropTypes.func.isRequired +} + +export default Mask diff --git a/isimip_data/download/assets/js/hooks/mutations.js b/isimip_data/download/assets/js/hooks/mutations.js index 374901e..f85e9c7 100644 --- a/isimip_data/download/assets/js/hooks/mutations.js +++ b/isimip_data/download/assets/js/hooks/mutations.js @@ -6,12 +6,13 @@ export const useSubmitJobMutation = () => { return useMutation({ mutationFn: (variables) => { - return DownloadApi.submitJob(variables.url, variables.data) + return DownloadApi.submitJob(variables.url, variables.data, variables.uploads) }, onSuccess: (data, variables) => { variables.setJob(data) }, onError: (error, variables) => { + console.log(error) variables.setErrors(error.errors) } }) diff --git a/isimip_data/download/assets/scss/download.scss b/isimip_data/download/assets/scss/download.scss index 5243dc9..f9ebce9 100644 --- a/isimip_data/download/assets/scss/download.scss +++ b/isimip_data/download/assets/scss/download.scss @@ -1,28 +1,6 @@ @import 'isimip_data/core/assets/scss/colors'; @import 'isimip_data/core/assets/scss/base'; -// .card { -// p { -// margin-bottom: 0.5rem; -// } -// .card-header { -// background-color: white; -// padding: 0.5rem 1.25rem; -// } -// } - -// form .form-check { -// margin-bottom: 0.5rem; - -// &:last-child { -// margin-bottom: 0; -// } - -// label.text-muted { -// font-weight: normal; -// } -// } - .symbols-spin { font-size: 24px; margin: -1px 6px -1px 0; @@ -110,9 +88,6 @@ .dropdown-operations { .dropdown-item { - white-space: normal; - max-width: 100%; - small { cursor: pointer; @@ -138,3 +113,17 @@ } } } + +.file-control { + height: auto; + + .file-control-inner { + cursor: pointer; + + height: 5rem; + padding: 0.25rem 0.5rem; + border: 1px dashed silver; + border-radius: 4px; + font-size: small; + } +} diff --git a/package-lock.json b/package-lock.json index b8f92e0..82620e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.4", "react-dom": "^18.3.1", + "react-dropzone": "^14.2.9", "react-markdown": "^9.0.1" }, "devDependencies": { @@ -2768,6 +2769,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==", + "engines": { + "node": ">=4" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -4112,6 +4121,17 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "dependencies": { + "tslib": "^2.4.0" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -6786,6 +6806,22 @@ "react": "^18.3.1" } }, + "node_modules/react-dropzone": { + "version": "14.2.9", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.9.tgz", + "integrity": "sha512-jRZsMC7h48WONsOLHcmhyn3cRWJoIPQjPApvt/sJVfnYaB3Qltn025AoRTTJaj4WdmmgmLl6tUQg1s0wOhpodQ==", + "dependencies": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "react": ">= 16.8 || 18.0.0" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -10340,6 +10376,11 @@ "is-shared-array-buffer": "^1.0.2" } }, + "attr-accept": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" + }, "available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -11283,6 +11324,14 @@ } } }, + "file-selector": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz", + "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==", + "requires": { + "tslib": "^2.4.0" + } + }, "fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -13063,6 +13112,16 @@ "scheduler": "^0.23.2" } }, + "react-dropzone": { + "version": "14.2.9", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.9.tgz", + "integrity": "sha512-jRZsMC7h48WONsOLHcmhyn3cRWJoIPQjPApvt/sJVfnYaB3Qltn025AoRTTJaj4WdmmgmLl6tUQg1s0wOhpodQ==", + "requires": { + "attr-accept": "^2.2.2", + "file-selector": "^0.6.0", + "prop-types": "^15.8.1" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index e0133ba..9873526 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.4", "react-dom": "^18.3.1", + "react-dropzone": "^14.2.9", "react-markdown": "^9.0.1" }, "devDependencies": {