Skip to content

Commit

Permalink
Bucket/PackageDialog: metadata dnd (#1950)
Browse files Browse the repository at this point in the history
  • Loading branch information
nl0 authored Dec 2, 2020
1 parent 86df77a commit e22cdcc
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 45 deletions.
5 changes: 2 additions & 3 deletions catalog/app/containers/Bucket/PackageCreateDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,7 @@ function PackageCreateDialog({ bucket, open, workflowsConfig, onClose, refresh }
scroll="body"
onExited={reset(form)}
>
<M.DialogTitle>
{success ? 'Package created' : 'Create package'}
</M.DialogTitle>
<M.DialogTitle>{success ? 'Package created' : 'Create package'}</M.DialogTitle>
{success ? (
<>
<M.DialogContent style={{ paddingTop: 0 }}>
Expand Down Expand Up @@ -564,6 +562,7 @@ function PackageCreateDialog({ bucket, open, workflowsConfig, onClose, refresh }
validate={validate}
validateFields={['meta']}
isEqual={R.equals}
initialValue={PD.EMPTY_META_VALUE}
/>
),
_: () => <PD.MetaInputSkeleton />,
Expand Down
203 changes: 161 additions & 42 deletions catalog/app/containers/Bucket/PackageDialog/PackageDialog.js
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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) => {
Expand All @@ -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 (
<div className={classes.root}>
<div className={classes.header}>
Expand All @@ -287,35 +379,62 @@ export function MetaInput({ schemaError, input, meta, schema }) {
</Lab.ToggleButtonGroup>
</div>

{value.mode === 'kv' ? (
<JsonEditor
error={error}
disabled={disabled}
value={parsedValue}
onChange={onJsonEditor}
schema={schema}
/>
) : (
<M.TextField
variant="outlined"
size="small"
value={value.text}
onChange={handleTextChange}
placeholder="Enter JSON metadata if necessary"
error={!!error}
helperText={
!!error &&
{
jsonObject: 'Metadata must be a valid JSON object',
}[error]
}
fullWidth
multiline
rowsMax={10}
InputProps={{ classes: { input: classes.jsonInput } }}
disabled={disabled}
/>
)}
<div {...getRootProps({ className: classes.dropzone })} tabIndex={undefined}>
{value.mode === 'kv' ? (
<JsonEditor
error={error}
disabled={disabled}
value={parsedValue}
onChange={onJsonEditor}
schema={schema}
key={jsonEditorKey}
/>
) : (
<M.TextField
variant="outlined"
size="small"
value={value.text}
onChange={handleTextChange}
placeholder="Enter JSON metadata if necessary"
error={!!error}
helperText={
!!error &&
{
jsonObject: 'Metadata must be a valid JSON object',
schema: 'Metadata must conform to the schema',
}[error]
}
fullWidth
multiline
rowsMax={10}
InputProps={{ classes: { input: classes.jsonInput } }}
disabled={disabled}
/>
)}

{(isDragActive || locked) && (
<div className={classes.overlay}>
{isDragActive ? (
<div className={classes.overlayContents}>
<div className={classes.overlayText}>
Drop file containing JSON metadata
</div>
</div>
) : (
<Delay ms={500} alwaysRender>
{(ready) => (
<M.Fade in={ready}>
<div className={classes.overlayContents}>
<M.CircularProgress size={20} className={classes.overlayProgress} />
<div className={classes.overlayText}>Reading file contents</div>
</div>
</M.Fade>
)}
</Delay>
)}
</div>
)}
</div>
</div>
)
}
Expand Down
1 change: 1 addition & 0 deletions catalog/app/containers/Bucket/PackageUpdateDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,7 @@ function DialogForm({
schema={schema}
schemaError={responseError}
validate={validate}
validateFields={['meta']}
isEqual={R.equals}
initialValue={initialMeta}
/>
Expand Down

0 comments on commit e22cdcc

Please sign in to comment.