From e22cdccd384ee017cf3dff9a1070f5e497ed96db Mon Sep 17 00:00:00 2001 From: Alexei Mochalov Date: Wed, 2 Dec 2020 19:51:54 +0500 Subject: [PATCH] Bucket/PackageDialog: metadata dnd (#1950) --- .../containers/Bucket/PackageCreateDialog.js | 5 +- .../Bucket/PackageDialog/PackageDialog.js | 203 ++++++++++++++---- .../containers/Bucket/PackageUpdateDialog.js | 1 + 3 files changed, 164 insertions(+), 45 deletions(-) diff --git a/catalog/app/containers/Bucket/PackageCreateDialog.js b/catalog/app/containers/Bucket/PackageCreateDialog.js index 03bd03c49bb..0ce419af2f2 100644 --- a/catalog/app/containers/Bucket/PackageCreateDialog.js +++ b/catalog/app/containers/Bucket/PackageCreateDialog.js @@ -473,9 +473,7 @@ function PackageCreateDialog({ bucket, open, workflowsConfig, onClose, refresh } scroll="body" onExited={reset(form)} > - - {success ? 'Package created' : 'Create package'} - + {success ? 'Package created' : 'Create package'} {success ? ( <> @@ -564,6 +562,7 @@ function PackageCreateDialog({ bucket, open, workflowsConfig, onClose, refresh } validate={validate} validateFields={['meta']} isEqual={R.equals} + initialValue={PD.EMPTY_META_VALUE} /> ), _: () => , diff --git a/catalog/app/containers/Bucket/PackageDialog/PackageDialog.js b/catalog/app/containers/Bucket/PackageDialog/PackageDialog.js index c955ae99003..a01fc69a6d2 100644 --- a/catalog/app/containers/Bucket/PackageDialog/PackageDialog.js +++ b/catalog/app/containers/Bucket/PackageDialog/PackageDialog.js @@ -1,16 +1,20 @@ import * as R from 'ramda' import * as React from 'react' +import { useDropzone } from 'react-dropzone' import * as M from '@material-ui/core' import * as Lab from '@material-ui/lab' import JsonEditor from 'components/JsonEditor' import { parseJSON, stringifyJSON, validateOnSchema } from 'components/JsonEditor/State' import Skeleton from 'components/Skeleton' +import * as Notifications from 'containers/Notifications' import { useData } from 'utils/Data' +import Delay from 'utils/Delay' import AsyncResult from 'utils/AsyncResult' import * as APIConnector from 'utils/APIConnector' import * as AWS from 'utils/AWS' import pipeThru from 'utils/pipeThru' +import { readableBytes } from 'utils/string' import * as validators from 'utils/validators' import * as workflows from 'utils/workflows' @@ -19,6 +23,7 @@ import SelectWorkflow from './SelectWorkflow' export const MAX_SIZE = 1000 * 1000 * 1000 // 1GB export const ES_LAG = 3 * 1000 +export const MAX_META_FILE_SIZE = 10 * 1000 * 1000 // 10MB export const ERROR_MESSAGES = { UPLOAD: 'Error uploading files', @@ -72,6 +77,21 @@ function cacheDebounce(fn, wait, getKey = R.identity) { } } +const readFile = (file) => + new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onabort = () => { + reject(new Error('abort')) + } + reader.onerror = () => { + reject(reader.error) + } + reader.onload = () => { + resolve(reader.result) + } + reader.readAsText(file) + }) + export function useNameValidator() { const req = APIConnector.use() const [counter, setCounter] = React.useState(0) @@ -104,16 +124,18 @@ function mkMetaValidator(schema) { return function validateMeta(value) { const noError = undefined + const jsonObjectErr = validators.jsonObject(value.text) + if (jsonObjectErr) { + return value.mode === 'json' + ? jsonObjectErr + : [{ message: 'Metadata must be a valid JSON object' }] + } + if (schema) { const obj = value ? parseJSON(value.text) : {} const errors = schemaValidator(obj) - return errors.length ? errors : noError - } - - if (!value) return noError - - if (value.mode === 'json') { - return validators.jsonObject(value.text) + if (!errors.length) return noError + return value.mode === 'json' ? 'schema' : errors } return noError @@ -226,14 +248,39 @@ const useMetaInputStyles = M.makeStyles((t) => ({ flexBasis: 100, flexGrow: 2, }, + dropzone: { + position: 'relative', + }, + overlay: { + background: 'rgba(255,255,255,0.6)', + bottom: 0, + left: 0, + position: 'absolute', + right: 0, + top: 0, + zIndex: 1, + }, + overlayContents: { + alignItems: 'center', + display: 'flex', + height: '100%', + justifyContent: 'center', + maxHeight: 120, + }, + overlayText: { + ...t.typography.body1, + color: t.palette.text.secondary, + }, + overlayProgress: { + marginRight: t.spacing(1), + }, })) -const EMPTY_META_VALUE = { mode: 'kv', text: '{}' } +export const EMPTY_META_VALUE = { mode: 'kv', text: '{}' } // TODO: warn on duplicate keys -export function MetaInput({ schemaError, input, meta, schema }) { +export function MetaInput({ schemaError, input: { value, onChange }, meta, schema }) { const classes = useMetaInputStyles() - const value = input.value || EMPTY_META_VALUE const error = schemaError ? [schemaError] : meta.submitFailed && meta.error const disabled = meta.submitting || meta.submitSucceeded @@ -244,15 +291,15 @@ export function MetaInput({ schemaError, input, meta, schema }) { const changeMode = (mode) => { if (disabled) return - input.onChange({ ...value, mode }) + onChange({ ...value, mode }) } const changeText = React.useCallback( (text) => { if (disabled) return - input.onChange({ ...value, text }) + onChange({ ...value, text }) }, - [disabled, input, value], + [disabled, onChange, value], ) const handleModeChange = (e, m) => { @@ -268,6 +315,51 @@ export function MetaInput({ schemaError, input, meta, schema }) { changeText, ]) + const { push: notify } = Notifications.use() + const [locked, setLocked] = React.useState(false) + // used to force json editor re-initialization + const [jsonEditorKey, setJsonEditorKey] = React.useState(1) + + const onDrop = React.useCallback( + ([file]) => { + if (file.size > MAX_META_FILE_SIZE) { + notify( + <> + File too large ({readableBytes(file.size)}), must be under{' '} + {readableBytes(MAX_META_FILE_SIZE)}. + , + ) + return + } + setLocked(true) + readFile(file) + .then((contents) => { + try { + JSON.parse(contents) + } catch (e) { + notify('The file does not contain valid JSON') + } + changeText(contents) + // force json editor to re-initialize + setJsonEditorKey(R.inc) + }) + .catch((e) => { + if (e.message === 'abort') return + // eslint-disable-next-line no-console + console.log('Error reading file') + // eslint-disable-next-line no-console + console.error(e) + notify("Couldn't read that file") + }) + .finally(() => { + setLocked(false) + }) + }, + [setLocked, changeText, setJsonEditorKey, notify], + ) + + const { getRootProps, isDragActive } = useDropzone({ onDrop }) + return (
@@ -287,35 +379,62 @@ export function MetaInput({ schemaError, input, meta, schema }) {
- {value.mode === 'kv' ? ( - - ) : ( - - )} +
+ {value.mode === 'kv' ? ( + + ) : ( + + )} + + {(isDragActive || locked) && ( +
+ {isDragActive ? ( +
+
+ Drop file containing JSON metadata +
+
+ ) : ( + + {(ready) => ( + +
+ +
Reading file contents
+
+
+ )} +
+ )} +
+ )} +
) } diff --git a/catalog/app/containers/Bucket/PackageUpdateDialog.js b/catalog/app/containers/Bucket/PackageUpdateDialog.js index 5bc0d9077bf..bc09f58df64 100644 --- a/catalog/app/containers/Bucket/PackageUpdateDialog.js +++ b/catalog/app/containers/Bucket/PackageUpdateDialog.js @@ -681,6 +681,7 @@ function DialogForm({ schema={schema} schemaError={responseError} validate={validate} + validateFields={['meta']} isEqual={R.equals} initialValue={initialMeta} />