Commit cba6d0f8 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Added publish with embargo to GUI. Added terms to GUI. Refactored...

Added publish with embargo to GUI. Added terms to GUI. Refactored commit->publish in API. Fixed read-only repo db bug.
parent 4ba06308
Pipeline #43155 passed with stages
in 18 minutes and 58 seconds
......@@ -6,39 +6,49 @@ import DialogActions from '@material-ui/core/DialogActions'
import DialogContent from '@material-ui/core/DialogContent'
import DialogContentText from '@material-ui/core/DialogContentText'
import DialogTitle from '@material-ui/core/DialogTitle'
import { FormGroup, Checkbox, FormLabel } from '@material-ui/core'
class ConfirmDialog extends React.Component {
static propTypes = {
onOk: PropTypes.func.isRequired,
onPublish: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
open: PropTypes.bool,
title: PropTypes.string,
children: PropTypes.any
open: PropTypes.bool.isRequired
}
render() {
const { children, title } = this.props
state = {
withEmbargo: false
}
render() {
const { onPublish, onClose, open } = this.props
const { withEmbargo } = this.state
return (
<div>
<Dialog
open={this.props.open}
onClose={this.handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
open={open}
onClose={onClose}
>
<DialogTitle id="alert-dialog-title">{title || 'Confirm'}</DialogTitle>
<DialogTitle>Publish data</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
{children}
<DialogContentText>
If you agree the selected uploads will move out of your private staging
area into the public nomad.
</DialogContentText>
<FormGroup row style={{alignItems: 'center'}}>
<Checkbox
checked={!withEmbargo}
onChange={() => this.setState({withEmbargo: !withEmbargo})}
/>
<FormLabel>publish without embargo</FormLabel>
</FormGroup>
</DialogContent>
<DialogActions>
<Button onClick={this.props.onClose} color="primary">
Disagree
<Button onClick={onClose} color="primary">
Cancel
</Button>
<Button onClick={this.props.onOk} color="primary" autoFocus>
Agree
<Button onClick={() => onPublish(withEmbargo)} color="primary" autoFocus>
{withEmbargo ? 'Publish with embargo' : 'Publish'}
</Button>
</DialogActions>
</Dialog>
......
......@@ -190,7 +190,7 @@ class Upload extends React.Component {
const { calcs, tasks, current_task, tasks_running, tasks_status, process_running, current_process, errors } = upload
// map tasks [ uploading, extracting, parse_all, cleanup ] to steps
const steps = [ 'upload', 'process', 'commit' ]
const steps = [ 'upload', 'process', 'publish' ]
let step = null
const task_index = tasks.indexOf(current_task)
if (task_index === 0) {
......@@ -198,7 +198,7 @@ class Upload extends React.Component {
} else if (task_index > 0 && tasks_running) {
step = 'process'
} else {
step = 'commit'
step = 'publish'
}
const stepIndex = steps.indexOf(step)
......@@ -278,11 +278,11 @@ class Upload extends React.Component {
)
}
},
commit: (props) => {
publish: (props) => {
props.children = 'inspect'
if (process_running) {
if (current_process === 'commit_upload') {
if (current_process === 'publish_upload') {
props.children = 'approved'
props.optional = <Typography variant="caption">moving data ...</Typography>
} else if (current_process === 'delete_upload') {
......@@ -290,7 +290,7 @@ class Upload extends React.Component {
props.optional = <Typography variant="caption">deleting data ...</Typography>
}
} else {
props.optional = <Typography variant="caption">commit or delete</Typography>
props.optional = <Typography variant="caption">publish or delete</Typography>
}
}
}
......
import React from 'react'
import PropTypes from 'prop-types'
import PropTypes, { instanceOf } from 'prop-types'
import Markdown from './Markdown'
import { withStyles, Paper, IconButton, FormGroup, Checkbox, FormControlLabel, FormLabel,
LinearProgress,
Typography} from '@material-ui/core'
Typography,
Tooltip} from '@material-ui/core'
import UploadIcon from '@material-ui/icons/CloudUpload'
import Dropzone from 'react-dropzone'
import Upload from './Upload'
......@@ -12,14 +13,16 @@ import DeleteIcon from '@material-ui/icons/Delete'
import ReloadIcon from '@material-ui/icons/Cached'
import CheckIcon from '@material-ui/icons/Check'
import ConfirmDialog from './ConfirmDialog'
import { Help } from './help'
import { Help, Agree } from './help'
import { withApi } from './api'
import { withCookies, Cookies } from 'react-cookie'
class Uploads extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired
raiseError: PropTypes.func.isRequired,
cookies: instanceOf(Cookies).isRequired
}
static styles = theme => ({
......@@ -72,7 +75,7 @@ class Uploads extends React.Component {
uploadCommand: 'loading ...',
selectedUploads: [],
loading: true,
showAccept: false
showPublish: false
}
componentDidMount() {
......@@ -107,15 +110,16 @@ class Uploads extends React.Component {
})
}
onAcceptClicked() {
this.setState({showAccept: true})
onPublishClicked() {
this.setState({showPublish: true})
}
handleAccept() {
onPublish(withEmbargo) {
this.setState({loading: true})
Promise.all(this.state.selectedUploads.map(upload => this.props.api.commitUpload(upload.upload_id)))
Promise.all(this.state.selectedUploads
.map(upload => this.props.api.publishUpload(upload.upload_id, withEmbargo)))
.then(() => {
this.setState({showAccept: false})
this.setState({showPublish: false})
return this.update()
})
.catch(error => {
......@@ -163,7 +167,7 @@ class Uploads extends React.Component {
renderUploads() {
const { classes } = this.props
const { selectedUploads } = this.state
const { selectedUploads, showPublish } = this.state
const uploads = this.state.uploads || []
return (<div>
......@@ -175,23 +179,36 @@ class Uploads extends React.Component {
onChange={(_, checked) => this.onSelectionAllChanged(checked)}
/>
)} />
<IconButton onClick={() => this.update()}><ReloadIcon /></IconButton>
<Tooltip title="reload uploads" >
<IconButton onClick={() => this.update()}><ReloadIcon /></IconButton>
</Tooltip>
<FormLabel classes={{root: classes.selectLabel}}>
{`selected uploads ${selectedUploads.length}/${uploads.length}`}
</FormLabel>
<IconButton
disabled={selectedUploads.length === 0}
onClick={this.onDeleteClicked.bind(this)}
>
<DeleteIcon />
</IconButton>
<Tooltip title="delete selected uploads" >
<div>
<IconButton
disabled={selectedUploads.length === 0}
onClick={this.onDeleteClicked.bind(this)}
>
<DeleteIcon />
</IconButton>
</div>
</Tooltip>
<Tooltip title="publish selected uploads" >
<div>
<IconButton disabled={selectedUploads.length === 0} onClick={() => this.onPublishClicked()}>
<CheckIcon />
</IconButton>
</div>
</Tooltip>
<IconButton disabled={selectedUploads.length === 0} onClick={this.onAcceptClicked.bind(this)}>
<CheckIcon />
</IconButton>
<ConfirmDialog open={this.state.showAccept} onClose={() => this.setState({showAccept: false})} onOk={this.handleAccept.bind(this)}>
If you agree the selected uploads will move out of your private staging area into the public nomad.
</ConfirmDialog>
<ConfirmDialog
open={showPublish}
onClose={() => this.setState({showPublish: false})}
onPublish={(withEmbargo) => this.onPublish(withEmbargo)}
/>
</FormGroup>
</div>
......@@ -200,11 +217,15 @@ class Uploads extends React.Component {
? (
<div>
<Help cookie="uploadList">{`
These are all your existing not commiting uploads. You can see how processing
progresses and review your uploads before commiting them to the *nomad repository*.
These are all your uploads in the *staging area*. You can see the
progress on data progresses and review your uploads before publishing
them to the *nomad repository*.
Select uploads to delete or commit them. Click on uploads to see individual
Select uploads to delete or publish them. Click on uploads to see individual
calculations. Click on calculations to see more details on each calculation.
When you select and click publish, you will be ask if you want to publish
with or without the optional *embargo period*.
`}</Help>
{
this.sortedUploads().map(upload => (
......@@ -224,46 +245,69 @@ class Uploads extends React.Component {
const { classes } = this.props
const { uploadCommand } = this.state
const agreement = `
By uploading and downloading data, you agree to the
[terms of use](https://www.nomad-coe.eu/the-project/nomad-repository/nomad-repository-terms).
Note that uploaded files become downloadable. Uploaded data is licensed under the
Creative Commons Attribution license ([CC BY 3.0](https://creativecommons.org/licenses/by/3.0/)).
You can put an *embargo* on uploaded data. The *embargo period* lasts up to 36 month.
If you do not decide on an *embargo* after upload, data will be made public after 48h
automatically.
`
return (
<div className={classes.root}>
<Typography variant="h4">Upload your own data</Typography>
<Help cookie="uploadHelp" component={Markdown}>{`
You can upload your own data. Have your code output ready in a popular archive
format (e.g. \`*.zip\` or \`*.tar.gz\`). Your upload can
comprise the output of multiple runs, even of different codes. Don't worry, nomad
will find it, just drop it below:
`}</Help>
<Agree message={agreement} cookie="agreedToUploadTerms">
<Help cookie="uploadHelp" component={Markdown}>{`
To upload your own data, please put all relevant files in a
\`*.zip\` or \`*.tar.gz\` archive. We encourage you to add all code input and
output files, as well as any other auxiliary files that you might have created.
You can put data from multiple calculations, using your preferred directory
structure, into your archives. Drop your archive file(s) below.
Uploaded data will not be public immediately. This is called the *staging area*.
After uploading and processing, you can decide if you want to make the data public,
delete it again, or put an *embargo* on it.
The *embargo* allows you to shared it with selected users, create a DOI
for your data, and later publish the data. The *embargo* might last up to
36 month before it becomes public automatically. During an *embargo*
some meta-data will be available.
`}</Help>
<Paper className={classes.dropzoneContainer}>
<Dropzone
accept={['application/zip', 'application/gzip', 'application/bz2']}
className={classes.dropzone}
activeClassName={classes.dropzoneAccept}
rejectClassName={classes.dropzoneReject}
onDrop={this.onDrop.bind(this)}
>
<p>drop files here</p>
<UploadIcon style={{fontSize: 36}}/>
</Dropzone>
</Paper>
<Paper className={classes.dropzoneContainer}>
<Dropzone
accept={['application/zip', 'application/gzip', 'application/bz2']}
className={classes.dropzone}
activeClassName={classes.dropzoneAccept}
rejectClassName={classes.dropzoneReject}
onDrop={this.onDrop.bind(this)}
>
<p>drop files here</p>
<UploadIcon style={{fontSize: 36}}/>
</Dropzone>
</Paper>
<Help cookie="uploadCommandHelp">{`
Alternatively, you can upload files via the following shell command.
Replace \`<local_file>\` with your file. After executing the command,
return here and reload.
`}</Help>
<Help cookie="uploadCommandHelp">{`
Alternatively, you can upload files via the following shell command.
Replace \`<local_file>\` with your archive file. After executing the command,
return here and reload (e.g. press the reload button below).
`}</Help>
<Markdown>{`
\`\`\`
${uploadCommand}
\`\`\`
`}</Markdown>
<Markdown>{`
\`\`\`
${uploadCommand}
\`\`\`
`}</Markdown>
{this.renderUploads()}
{this.state.loading ? <LinearProgress/> : ''}
{this.renderUploads()}
{this.state.loading ? <LinearProgress/> : ''}
</Agree>
</div>
)
}
}
export default compose(withApi(true), withStyles(Uploads.styles))(Uploads)
export default compose(withApi(true), withCookies, withStyles(Uploads.styles))(Uploads)
......@@ -215,12 +215,15 @@ class Api {
.then(response => response.body)
}
async commitUpload(uploadId) {
async publishUpload(uploadId, withEmbargo) {
const client = await this.swaggerPromise
return client.apis.uploads.exec_upload_command({
return client.apis.uploads.exec_upload_operation({
upload_id: uploadId,
payload: {
command: 'commit'
operation: 'publish',
metadata: {
with_embargo: withEmbargo
}
}
})
.catch(this.handleApiError)
......
import React from 'react'
import { withStyles, Button } from '@material-ui/core'
import { withStyles, Button, Collapse, Fade } from '@material-ui/core'
import Markdown from './Markdown'
import PropTypes, { instanceOf } from 'prop-types'
import { Cookies, withCookies } from 'react-cookie'
import classNames from 'classnames'
export const HelpContext = React.createContext()
......@@ -51,17 +52,98 @@ class HelpProviderComponent extends React.Component {
}
}
class HelpComponent extends React.Component {
export class Help extends React.Component {
static propTypes = {
children: PropTypes.any,
cookie: PropTypes.string.isRequired
}
render() {
const { children, cookie } = this.props
return (
<HelpContext.Consumer>{
help => (
<Collapse in={help.isOpen(cookie)}>
<GotIt onGotIt={() => help.gotIt(cookie)}>
{children}
</GotIt>
</Collapse>
)
}</HelpContext.Consumer>
)
}
}
class AgreeComponent extends React.Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.node, PropTypes.arrayOf(PropTypes.node)
]).isRequired,
message: PropTypes.string.isRequired,
cookie: PropTypes.string.isRequired,
cookies: instanceOf(Cookies).isRequired
}
state = {
agreed: this.props.cookies.get(this.props.cookie)
}
onAgreeClicked() {
this.props.cookies.set(this.props.cookie, true)
this.setState({agreed: true})
}
render() {
const { children, message } = this.props
const { agreed } = this.state
return (
<div>
<Collapse in={!agreed}>
<GotIt onGotIt={() => this.onAgreeClicked()} color="error">
{message}
</GotIt>
</Collapse>
<Fade in={!!agreed}>
<div>
{children}
</div>
</Fade>
</div>
)
}
}
export const Agree = withCookies(AgreeComponent)
class GotItUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
onGotIt: PropTypes.func.isRequired,
children: PropTypes.node.isRequired,
color: PropTypes.oneOf(['primary', 'error']).isRequired
}
static defaultProps = {
color: 'primary'
}
static styles = theme => ({
root: {
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2,
borderRadius: theme.spacing.unit * 0.5,
border: `1px solid ${theme.palette.primary.main}`,
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
rootPrimary: {
border: `1px solid ${theme.palette.primary.main}`
},
rootError: {
border: `1px solid ${theme.palette.error.main}`
},
content: {
paddingLeft: theme.spacing.unit * 2,
flex: '1 1 auto'
......@@ -72,34 +154,27 @@ class HelpComponent extends React.Component {
}
})
static propTypes = {
classes: PropTypes.object.isRequired,
children: PropTypes.any,
cookie: PropTypes.string.isRequired
}
render() {
const { classes, children, cookie } = this.props
const { classes, children, onGotIt, color } = this.props
const rootClassName = classNames(classes.root, {
[classes.rootPrimary]: color === 'primary',
[classes.rootError]: color === 'error'
})
return (
<HelpContext.Consumer>{
help => (
help.isOpen(cookie)
? <div className={classes.root}>
<div className={classes.content}>
<Markdown>
{children}
</Markdown>
</div>
<div className={classes.actions}>
<Button color="primary" onClick={() => help.gotIt(cookie)}>Got it</Button>
</div>
</div> : ''
)
}</HelpContext.Consumer>
<div className={rootClassName}>
<div className={classes.content}>
<Markdown>
{children}
</Markdown>
</div>
<div className={classes.actions}>
<Button color="primary" onClick={onGotIt}>Got it</Button>
</div>
</div>
)
}
}
const GotIt = withStyles(GotItUnstyled.styles)(GotItUnstyled)
export const HelpProvider = withCookies(HelpProviderComponent)
export const Help = withStyles(HelpComponent.styles)(HelpComponent)
......@@ -103,8 +103,8 @@ upload_with_calcs_model = api.inherit('UploadWithPaginatedCalculations', upload_
}))
})
upload_command_model = api.model('UploadCommand', {
'command': fields.String(description='Currently commit is the only command.'),
upload_operation_model = api.model('UploadOperation', {
'operation': fields.String(description='Currently publish is the only operation.'),
'metadata': fields.Nested(model=upload_metadata_model, description='Additional upload and calculation meta data. Will replace previously given metadata.')
})
......@@ -300,20 +300,20 @@ class UploadResource(Resource):
return upload, 200
@api.doc('exec_upload_command')
@api.doc('exec_upload_operation')
@api.response(404, 'Upload does not exist or not in staging')
@api.response(400, 'Operation is not supported or the upload is still/already processed')
@api.response(401, 'If the command is not allowed for the current user')
@api.marshal_with(upload_model, skip_none=True, code=200, description='Upload commited successfully')
@api.expect(upload_command_model)
@api.response(401, 'If the operation is not allowed for the current user')
@api.marshal_with(upload_model, skip_none=True, code=200, description='Upload published successfully')
@api.expect(upload_operation_model)
@login_really_required
def post(self, upload_id):
"""
Execute an upload command. Available operations: ``commit``
Execute an upload operation. Available operations: ``publish``
Unstage accepts further meta data that allows to provide coauthors, comments,
external references, etc. See the model for details. The fields that start with
``_underscore`` are only available for users with administrative priviledges.
``_underscore`` are only available for users with administrative privileges.
Unstage changes the visibility of the upload. Clients can specify the visibility
via meta data.
......@@ -330,7 +330,7 @@ class UploadResource(Resource):
if json_data is None:
json_data = {}
command = json_data.get('command')
operation = json_data.get('operation')
metadata = json_data.get('metadata', {})
for key in metadata:
......@@ -339,20 +339,20 @@ class UploadResource(Resource):
abort(401, message='Only admin users can use _metadata_keys.')
break
if command == 'commit':
if operation == 'publish':
if upload.tasks_running:
abort(400, message='The upload is not processed yet')
if upload.tasks_status == FAILURE:
abort(400, message='Cannot commit an upload that failed processing')
abort(400, message='Cannot publish an upload that failed processing')
try:
upload.metadata = metadata
upload.commit_upload()
upload.publish_upload()
except ProcessAlreadyRunning:
abort(400, message='The upload is still/already processed')
return upload, 200
abort(400, message='Unsuported command %s.' % command)