diff --git a/gui/src/components/App.js b/gui/src/components/App.js index 089b7f8a6b3a13f5b6f34fe360c3f6a917fdcfec..59d4c93f32859b4c2134f7e8c36f913e21be0c76 100644 --- a/gui/src/components/App.js +++ b/gui/src/components/App.js @@ -68,7 +68,9 @@ class NavigationUnstyled extends React.Component { } static styles = theme => ({ - root: {}, + root: { + minWidth: 1024, + }, title: { marginLeft: theme.spacing.unit, flexGrow: 1, diff --git a/gui/src/components/DataTable.js b/gui/src/components/DataTable.js index bcb528ca35de0b0075627a607bc4dc6a82edeb1b..b56735e41424dd6e6f9c5578e5364dcb2b69fccb 100644 --- a/gui/src/components/DataTable.js +++ b/gui/src/components/DataTable.js @@ -32,8 +32,7 @@ class DataTableToolbarUnStyled extends React.Component { static styles = theme => ({ root: { - paddingLeft: theme.spacing.unit * 2, - paddingRight: theme.spacing.unit + paddingLeft: theme.spacing.unit * 3 }, selected: { color: theme.palette.secondary.main diff --git a/gui/src/components/api.js b/gui/src/components/api.js index 3dfdbec4c0b9915a5f6df95aad8dd29534860df6..ea5ef9337db37751349108f0cb315bd556aa6b4b 100644 --- a/gui/src/components/api.js +++ b/gui/src/components/api.js @@ -217,10 +217,14 @@ class Api { return upload } - async getUnpublishedUploads() { + async getUploads(state, page, perPage) { + state = state || 'all' + page = page || 1 + perPage = perPage || 10 + this.onStartLoading() return this.swagger() - .then(client => client.apis.uploads.get_uploads({state: 'unpublished', page: 1, per_page: 1000})) + .then(client => client.apis.uploads.get_uploads({state: state, page: page, per_page: perPage})) .catch(handleApiError) .then(response => ({ ...response.body, @@ -233,20 +237,12 @@ class Api { .finally(this.onFinishLoading) } + async getUnpublishedUploads() { + return this.getUploads('unpublished', 1, 1000) + } + async getPublishedUploads(page, perPage) { - this.onStartLoading() - return this.swagger() - .then(client => client.apis.uploads.get_uploads({state: 'published', page: page || 1, per_page: perPage || 10})) - .catch(handleApiError) - .then(response => ({ - ...response.body, - results: response.body.results.map(uploadJson => { - const upload = new Upload(uploadJson, this) - upload.uploading = 100 - return upload - }) - })) - .finally(this.onFinishLoading) + return this.getUploads('published', 1, 10) } async archive(uploadId, calcId) { diff --git a/gui/src/components/search/EntryList.js b/gui/src/components/search/EntryList.js index 44a7197cd1685c9758157cec7700112bcc72e186..6cdbf0c9de8196347b4f47cb33c68c9607f28d99 100644 --- a/gui/src/components/search/EntryList.js +++ b/gui/src/components/search/EntryList.js @@ -25,6 +25,8 @@ export class EntryListUnstyled extends React.Component { domain: PropTypes.object.isRequired, editable: PropTypes.bool, columns: PropTypes.object, + title: PropTypes.string, + actions: PropTypes.element, selectedColumns: PropTypes.arrayOf(PropTypes.string) } @@ -38,9 +40,6 @@ export class EntryListUnstyled extends React.Component { entryDetailsRow: { paddingRight: theme.spacing.unit * 3 }, - clickableRow: { - cursor: 'pointer' - } }) state = { @@ -230,7 +229,7 @@ export class EntryListUnstyled extends React.Component { } render() { - const { classes, data, order, order_by, page, per_page, domain, editable } = this.props + const { classes, data, order, order_by, page, per_page, domain, editable, title, ...rest } = this.props const { results, pagination: { total } } = data const { selected } = this.state @@ -269,7 +268,7 @@ export class EntryListUnstyled extends React.Component { return ( <div className={classes.root}> <DataTable - title={`${total.toLocaleString()} ${domain.entryLabel}s`} + title={title || `${total.toLocaleString()} ${domain.entryLabel}s`} selectActions={selectActions} id={row => row.calc_id} total={total} @@ -285,6 +284,7 @@ export class EntryListUnstyled extends React.Component { onOrderChanged={(order, orderBy) => this.handleChange({order: order === 'asc' ? -1 : 1, order_by: orderBy})} rows={per_page} pagination={pagination} + {...rest} /> </div> ) diff --git a/gui/src/components/uploads/Upload.js b/gui/src/components/uploads/Upload.js index 705887d238afb3a1f8765a697ee2a24c47d94d94..b2fc5337cba79cf221d62e383630deb0706648ab 100644 --- a/gui/src/components/uploads/Upload.js +++ b/gui/src/components/uploads/Upload.js @@ -3,7 +3,8 @@ import PropTypes from 'prop-types' import { withStyles, ExpansionPanel, ExpansionPanelSummary, Typography, ExpansionPanelDetails, Stepper, Step, StepLabel, Checkbox, FormControlLabel, Tooltip, - CircularProgress} from '@material-ui/core' + CircularProgress, + IconButton} from '@material-ui/core' import ExpandMoreIcon from '@material-ui/icons/ExpandMore' import ReactJson from 'react-json-view' import { compose } from 'recompose' @@ -12,17 +13,21 @@ import { withRouter } from 'react-router' import { debug } from '../../config' import EntryList, { EntryListUnstyled } from '../search/EntryList' import { withDomain } from '../domains' +import DeleteIcon from '@material-ui/icons/Delete' +import PublishIcon from '@material-ui/icons/Publish' +import ConfirmDialog from './ConfirmDialog' +import PublishedIcon from '@material-ui/icons/Visibility' +import UnPublishedIcon from '@material-ui/icons/Lock' +import { withApi } from '../api' class Upload extends React.Component { static propTypes = { classes: PropTypes.object.isRequired, raiseError: PropTypes.func.isRequired, + api: PropTypes.func.isRequired, upload: PropTypes.object.isRequired, - checked: PropTypes.bool, - onCheckboxChanged: PropTypes.func, onDoesNotExist: PropTypes.func, onPublished: PropTypes.func, - history: PropTypes.any.isRequired, domain: PropTypes.object.isRequired, } @@ -39,9 +44,6 @@ class Upload extends React.Component { display: 'block', overflowX: 'auto' }, - summary: { - overflowX: 'auto' - }, detailsContent: { margin: theme.spacing.unit * 3 }, @@ -75,7 +77,7 @@ class Upload extends React.Component { whiteSpace: 'nowrap', textAlign: 'right' }, - progress: { + icon: { marginLeft: -theme.spacing.unit * 0.5, width: theme.spacing.unit * 13 - 2, alignItems: 'center', @@ -105,6 +107,10 @@ class Upload extends React.Component { constructor(props) { super(props) this.handleChange = this.handleChange.bind(this) + this.handleDelete = this.handleDelete.bind(this) + this.handlePublishCancel = this.handlePublishCancel.bind(this) + this.handlePublishOpen = this.handlePublishOpen.bind(this) + this.handlePublishSubmit = this.handlePublishSubmit.bind(this) } componentDidUpdate(prevProps, prevState) { @@ -236,6 +242,38 @@ class Upload extends React.Component { this.setState({params: {...this.state.params, ...changes}}) } + handleDelete() { + const { api, upload } = this.props + api.deleteUpload(upload.upload_id) + .then(() => this.update()) + .catch(error => { + this.props.raiseError(error) + this.update() + }) + } + + handlePublishOpen() { + this.setState({showPublishDialog: true}) + } + + handlePublishSubmit(withEmbargo) { + const { api, upload } = this.props + api.publishUpload(upload.upload_id, withEmbargo) + .then(() => { + this.setState({showPublishDialog: false}) + this.update() + }) + .catch(error => { + this.props.raiseError(error) + this.setState({showPublishDialog: false}) + this.update() + }) + } + + handlePublishCancel() { + this.setState({showPublishDialog: false}) + } + onCheckboxChanged(_, checked) { if (this.props.onCheckboxChanged) { this.props.onCheckboxChanged(checked) @@ -400,7 +438,7 @@ class Upload extends React.Component { renderCalcTable() { const { classes } = this.props - const { columns } = this.state + const { columns, upload } = this.state const { calcs, tasks_status, waiting } = this.state.upload const { pagination, results } = calcs @@ -435,53 +473,68 @@ class Upload extends React.Component { })) } + const running = upload.tasks_running || upload.process_running + + const actions = upload.published ? '' : <React.Fragment> + <IconButton> + <Tooltip title="Delete upload" disable={running} onClick={this.handleDelete}> + <DeleteIcon /> + </Tooltip> + </IconButton> + <IconButton disable={running || tasks_status !== 'SUCCESS'} onClick={this.handlePublishOpen}> + <Tooltip title="Publish upload"> + <PublishIcon /> + </Tooltip> + </IconButton> + </React.Fragment> + return <EntryList + title={`Upload with ${data.pagination.total} detected entries`} query={{upload_id: this.props.upload_id}} columns={columns} selectedColumns={Upload.defaultSelectedColumns} - editable + editable={tasks_status === 'SUCCESS'} data={data} onChange={this.handleChange} + actions={actions} {...this.state.params} /> } - renderCheckBox() { + renderStatusIcon() { const { classes } = this.props const { upload } = this.state - if (upload.tasks_running || upload.process_running) { - return <div className={classes.progress}> - <CircularProgress size={32}/> + const render = (icon, tooltip) => ( + <div className={classes.icon}> + <Tooltip title={tooltip}> + {icon} + </Tooltip> </div> - } else if (!upload.published) { - return <FormControlLabel control={( - <Checkbox - checked={this.props.checked} - className={classes.checkbox} - onClickCapture={(e) => e.stopPropagation()} - onChange={this.onCheckboxChanged.bind(this)} - /> - )}/> + ) + + if (upload.tasks_running || upload.process_running) { + return render(<CircularProgress size={32}/>, '') + } else if (upload.published) { + return render(<PublishedIcon size={32} color="action"/>, 'This upload is published') } else { - return '' + return render(<UnPublishedIcon size={32} color="primary"/>, 'This upload is not published yet, and only visible to you') } } render() { const { classes } = this.props - const { upload } = this.state + const { upload, showPublishDialog } = this.state const { errors } = upload if (this.state.upload) { return ( <div className={classes.root}> <ExpansionPanel> - <ExpansionPanelSummary - expandIcon={<ExpandMoreIcon/>} - classes={{root: classes.summary}}> - - {this.renderCheckBox()} {this.renderTitle()} {this.renderStepper()} + <ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} > + {this.renderStatusIcon()} + {this.renderTitle()} + {this.renderStepper()} </ExpansionPanelSummary> <ExpansionPanelDetails style={{width: '100%'}} classes={{root: classes.details}}> {errors && errors.length > 0 @@ -496,6 +549,11 @@ class Upload extends React.Component { </div> : ''} </ExpansionPanelDetails> </ExpansionPanel> + <ConfirmDialog + open={showPublishDialog} + onClose={this.handlePublishCancel} + onPublish={this.handlePublishSubmit} + /> </div> ) } else { @@ -504,4 +562,4 @@ class Upload extends React.Component { } } -export default compose(withRouter, withErrors, withDomain, withStyles(Upload.styles))(Upload) +export default compose(withRouter, withErrors, withApi(true, false), withDomain, withStyles(Upload.styles))(Upload) diff --git a/gui/src/components/uploads/Uploads.js b/gui/src/components/uploads/Uploads.js index 5de37578e96e34e1325fd9d87df236fa550eb2f3..438c92e5c3aa9ce5500849223eab28302f5321e0 100644 --- a/gui/src/components/uploads/Uploads.js +++ b/gui/src/components/uploads/Uploads.js @@ -7,8 +7,8 @@ import Dropzone from 'react-dropzone' import Upload from './Upload' import { compose } from 'recompose' import DeleteIcon from '@material-ui/icons/Delete' -import ReloadIcon from '@material-ui/icons/Cached' import CheckIcon from '@material-ui/icons/Check' +import ReloadIcon from '@material-ui/icons/Cached' import MoreIcon from '@material-ui/icons/MoreHoriz' import ClipboardIcon from '@material-ui/icons/Assignment' import ConfirmDialog from './ConfirmDialog' @@ -139,16 +139,15 @@ class Uploads extends React.Component { marginRight: theme.spacing.unit, overflow: 'hidden' }, - selectFormGroup: { - paddingLeft: theme.spacing.unit * 3 + formGroup: { + paddingLeft: 0 }, - selectLabel: { + uploadsLabel: { + flexGrow: 1, + paddingLeft: 0, padding: theme.spacing.unit * 2 }, uploads: { - marginTop: theme.spacing.unit * 2 - }, - uploadsContainer: { marginTop: theme.spacing.unit * 4 }, pagination: { @@ -156,18 +155,23 @@ class Uploads extends React.Component { } }) + defaultData = { + results: [], + pagination: { + total: 0, + per_page: 10, + page: 1 + } + } + state = { - unpublishedUploads: null, - publishedUploads: null, - publishedUploadsPage: 1, - publishedUploadsTotal: 0, uploadCommand: { upload_command: 'loading ...', upload_tar_command: 'loading ...', upload_progress_command: 'loading ...' }, - selectedUnpublishedUploads: [], - showPublishDialog: false + data: {...this.defaultData}, + uploading: [] } componentDidMount() { @@ -181,77 +185,33 @@ class Uploads extends React.Component { }) } - update(publishedUploadsPage) { - this.props.api.getUnpublishedUploads() - .then(uploads => { - // const filteredUploads = uploads.filter(upload => !upload.is_state) - this.setState({unpublishedUploads: uploads.results, selectedUnpublishedUploads: []}) - }) - .catch(error => { - this.setState({unpublishedUploads: [], selectedUnpublishedUploads: []}) - this.props.raiseError(error) - }) - this.props.api.getPublishedUploads(publishedUploadsPage, publishedUploadsPageSize) + update(newPage) { + const { data: { pagination: { page, per_page }}} = this.state + this.props.api.getUploads('all', newPage || page, per_page) .then(uploads => { - this.setState({ - publishedUploads: uploads.results, - publishedUploadsTotal: uploads.pagination.total, - publishedUploadsPage: uploads.pagination.page}) - }) - .catch(error => { - this.setState({publishedUploads: []}) - this.props.raiseError(error) - }) - } - - onDeleteClicked() { - Promise.all(this.state.selectedUnpublishedUploads.map(upload => this.props.api.deleteUpload(upload.upload_id))) - .then(() => this.update()) - .catch(error => { - this.props.raiseError(error) - this.update() - }) - } - - onPublishClicked() { - this.setState({showPublishDialog: true}) - } - - onPublish(withEmbargo) { - Promise.all(this.state.selectedUnpublishedUploads - .map(upload => this.props.api.publishUpload(upload.upload_id, withEmbargo))) - .then(() => { - this.setState({showPublishDialog: false}) - return this.update() + this.setState({data: uploads}) }) .catch(error => { + this.setState({data: {...this.defaultData}}) this.props.raiseError(error) - this.update() }) } - sortedUnpublishedUploads(order) { - order = order || -1 - return this.state.unpublishedUploads.concat() - .sort((a, b) => (a.gui_upload_id === b.gui_upload_id) - ? 0 - : ((a.gui_upload_id < b.gui_upload_id) ? -1 : 1) * order) - } - handleDoesNotExist(nonExistingUpload) { - this.setState({ - unpublishedUploads: this.state.unpublishedUploads.filter(upload => upload !== nonExistingUpload) - }) + // this.setState({ + // unpublishedUploads: this.state.unpublishedUploads.filter(upload => upload !== nonExistingUpload) + // }) + this.update() } - handlePublished(publishedUpload) { + handlePublished() { this.update() } onDrop(files, rejectedFiles) { const upload = file => { const upload = this.props.api.createUpload(file.name) - this.setState({unpublishedUploads: [...this.state.unpublishedUploads, upload]}) + this.setState({uploading: [...this.state.uploading, upload]}) upload.uploadFile(file).catch(this.props.raiseError) } @@ -261,129 +221,38 @@ class Uploads extends React.Component { .forEach(upload) } - onSelectionChanged(upload, checked) { - if (checked) { - this.setState({selectedUnpublishedUploads: [upload, ...this.state.selectedUnpublishedUploads]}) - } else { - const selectedUnpublishedUploads = [...this.state.selectedUnpublishedUploads] - selectedUnpublishedUploads.splice(selectedUnpublishedUploads.indexOf(upload), 1) - this.setState({selectedUnpublishedUploads: selectedUnpublishedUploads}) - } - } - - onSelectionAllChanged(checked) { - if (checked) { - this.setState({selectedUnpublishedUploads: [...this.state.unpublishedUploads.filter(upload => !upload.tasks_running)]}) - } else { - this.setState({selectedUnpublishedUploads: []}) - } - } - - renderPublishedUploads() { + renderUploads() { const { classes } = this.props - const { publishedUploadsTotal, publishedUploadsPage, publishedUploads } = this.state + const { data: { results, pagination: { total, per_page, page }}, uploading } = this.state - if (!publishedUploads || publishedUploads.length === 0) { + if (total === 0) { return '' } - return (<div className={classes.uploadsContainer}> - <FormLabel className={classes.uploadsLabel}>Your published uploads: </FormLabel> - <div className={classes.uploads}> - <div> - { - publishedUploads.map(upload => ( - <Upload key={upload.gui_upload_id} upload={upload} - checked={false} - onCheckboxChanged={checked => true}/> - )) - } - { - (publishedUploadsTotal > publishedUploadsPageSize) - ? <Pagination classes={{root: classes.pagination}} - limit={publishedUploadsPageSize} - offset={(publishedUploadsPage - 1) * publishedUploadsPageSize} - total={publishedUploadsTotal} - onClick={(_, offset) => this.update((offset / publishedUploadsPageSize) + 1)} - previousPageLabel={'prev'} - nextPageLabel={'next'} - /> : '' - } - </div> - </div> - </div>) - } - - renderUnpublishedUploads() { - const { classes } = this.props - const { selectedUnpublishedUploads, showPublishDialog } = this.state - const unpublishedUploads = this.state.unpublishedUploads || [] - - const reloadButton = <Tooltip title="Reload uploads, e.g. after using the curl upload" > - <IconButton onClick={() => this.update()}><ReloadIcon /></IconButton> - </Tooltip> - - return (<div className={classes.uploadsContainer}> - <div style={{width: '100%'}}> - {(unpublishedUploads.length === 0) ? '' - : <FormLabel className={classes.uploadsLabel}>Your unpublished uploads: </FormLabel> - } - <FormGroup className={classes.selectFormGroup} style={{alignItems: 'center'}}row> - {(unpublishedUploads.length === 0) ? <FormLabel label="all" style={{flexGrow: 1}}>You have currently no unpublished uploads</FormLabel> - : <FormControlLabel label="all" style={{flexGrow: 1}} control={( - <Checkbox - checked={selectedUnpublishedUploads.length === unpublishedUploads.length && unpublishedUploads.length !== 0} - onChange={(_, checked) => this.onSelectionAllChanged(checked)} - /> - )} /> - } - {reloadButton} - <FormLabel classes={{root: classes.selectLabel}}> - {`selected uploads ${selectedUnpublishedUploads.length}/${unpublishedUploads.length}`} - </FormLabel> - <Tooltip title="Delete selected uploads" > - <div> - <IconButton - disabled={selectedUnpublishedUploads.length === 0} - onClick={this.onDeleteClicked.bind(this)} - > - <DeleteIcon /> - </IconButton> - </div> - </Tooltip> - - <Tooltip title="Publish selected uploads" > - <div> - <IconButton - disabled={selectedUnpublishedUploads.length === 0 || selectedUnpublishedUploads.some(upload => upload.tasks_status !== 'SUCCESS' || upload.total_calcs === 0)} - onClick={() => this.onPublishClicked()}> - <CheckIcon /> - </IconButton> - </div> - </Tooltip> - - <ConfirmDialog - open={showPublishDialog} - onClose={() => this.setState({showPublishDialog: false})} - onPublish={(withEmbargo) => this.onPublish(withEmbargo)} - /> - - </FormGroup> - </div> - { - (unpublishedUploads.length === 0) - ? '' - : <div className={classes.uploads}> - { this.sortedUnpublishedUploads().map(upload => ( - <Upload key={upload.gui_upload_id} upload={upload} - checked={selectedUnpublishedUploads.indexOf(upload) !== -1} - onDoesNotExist={() => this.handleDoesNotExist(upload)} - onPublished={() => this.handlePublished(upload)} - onCheckboxChanged={checked => this.onSelectionChanged(upload, checked)}/> - )) - } - </div> - } + const renderUpload = upload => <Upload + key={upload.gui_upload_id} upload={upload} + onDoesNotExist={() => this.handleDoesNotExist(upload)} + onPublished={() => this.handlePublished(upload)} + /> + + return (<div className={classes.uploads}> + <FormGroup className={classes.formGroup} row> + <FormLabel className={classes.uploadsLabel}>Your uploads: </FormLabel> + <Tooltip title="Reload uploads, e.g. after using the curl upload" > + <IconButton onClick={() => this.update()}><ReloadIcon /></IconButton> + </Tooltip> + </FormGroup> + {uploading.map(renderUpload)} + {results.map(renderUpload)} + {(total > per_page) + ? <Pagination classes={{root: classes.pagination}} + limit={per_page} + offset={(page - 1) * per_page} + total={total} + onClick={(_, offset) => this.update((offset / per_page) + 1)} + previousPageLabel={'prev'} + nextPageLabel={'next'} + /> : ''} </div>) } @@ -471,8 +340,7 @@ class Uploads extends React.Component { `}/> </div> - {this.renderUnpublishedUploads()} - {this.renderPublishedUploads()} + {this.renderUploads()} </div> ) } diff --git a/nomad/app/api/upload.py b/nomad/app/api/upload.py index e058447b51816bdff9c1df36f97e5db1d8ea3f03..a98a9cd3b281aad235a138e3c99359bda860555d 100644 --- a/nomad/app/api/upload.py +++ b/nomad/app/api/upload.py @@ -206,7 +206,7 @@ class UploadListResource(Resource): results = [ upload - for upload in uploads.order_by('-upload_time')[(page - 1) * per_page: page * per_page]] + for upload in uploads.order_by('published', '-upload_time')[(page - 1) * per_page: page * per_page]] return dict( pagination=dict(total=total, page=page, per_page=per_page),