diff --git a/gui/src/api.js b/gui/src/api.js index cf10a9a855b71c6d7d8d18c7aa6254c13c9ceba3..f7f00a2f6deb282be6829c133213575b58a880ef 100644 --- a/gui/src/api.js +++ b/gui/src/api.js @@ -62,22 +62,30 @@ class Upload { _assignFromJson(uploadJson, created) { Object.assign(this, uploadJson) - if (this.proc.current_task_name !== this.proc.task_names[0]) { + if (this.current_task !== this.tasks[0]) { this.uploading = 100 } else if (!created && this.uploading === null) { // if data came from server during a normal get (not create) and its still uploading // and the uploading is also not controlled locally then it ought to be a failure/abort - this.proc.status = 'FAILURE' - this.is_ready = true - this.proc.errors = ['upload failed, probably aborted'] + this.status = 'FAILURE' + this.completed = true + this.errors = ['upload failed, probably aborted'] } } - update() { + get(page, perPage, orderBy, order) { + if (!page) page = 1 + if (!perPage) perPage = 5 + if (!orderBy) orderBy = 'mainfile' + if (!order) order = 'desc' + + order = order === 'desc' ? -1 : 1 + if (this.uploading !== null && this.uploading !== 100) { return new Promise(resolve => resolve(this)) } else { - return fetch(`${apiBase}/uploads/${this.upload_id}`) + const qparams = `page=${page}&per_page=${perPage}&order_by=${orderBy}&order=${order}` + return fetch(`${apiBase}/uploads/${this.upload_id}?${qparams}`) .catch(networkError) .then(handleResponseErrors) .then(response => response.json()) @@ -148,7 +156,7 @@ async function getMetaInfo() { if (cachedMetaInfo) { return cachedMetaInfo } else { - const loadMetaInfo = async (path) => { + const loadMetaInfo = async(path) => { return fetch(`${appStaticBase}/metainfo/meta_info/nomad_meta_info/${path}`) .catch(networkError) .then(handleResponseErrors) diff --git a/gui/src/components/CalcLinks.js b/gui/src/components/CalcLinks.js index a5f1d80232dae0956c79e614c39ee38df3629188..842e67d7f83d3d7e56643201ca769cefff3c7fa8 100644 --- a/gui/src/components/CalcLinks.js +++ b/gui/src/components/CalcLinks.js @@ -10,32 +10,33 @@ import Link from 'react-router-dom/Link' class CalcLink extends React.Component { static propTypes = { classes: PropTypes.object.isRequired, - uploadHash: PropTypes.string.isRequired, - calcHash: PropTypes.string.isRequired + calcId: PropTypes.string, + uploadHash: PropTypes.string, + calcHash: PropTypes.string, + disabled: PropTypes.bool } static styles = theme => ({ root: { overflow: 'hidden', - whiteSpace: 'nowrap', - textAlign: 'right' + whiteSpace: 'nowrap' } }); render() { - const { uploadHash, calcHash, classes } = this.props - const archiveId = `${uploadHash}/${calcHash}` + const { uploadHash, calcHash, classes, calcId, disabled } = this.props + const id = calcId || `${uploadHash}/${calcHash}` return ( <div className={classes.root}> <MuiThemeProvider theme={repoTheme}> - <IconButton color="primary" component={Link} to={`/repo/${archiveId}`}><RepoIcon /></IconButton> + <IconButton color="primary" component={Link} to={`/repo/${id}`} disabled={disabled}><RepoIcon /></IconButton> </MuiThemeProvider> <MuiThemeProvider theme={archiveTheme}> - <IconButton color="primary" component={Link} to={`/archive/${archiveId}`}><ArchiveIcon /></IconButton> + <IconButton color="primary" component={Link} to={`/archive/${id}`} disabled={disabled}><ArchiveIcon /></IconButton> </MuiThemeProvider> <MuiThemeProvider theme={encTheme}> - <IconButton color="primary" component={Link} to={`/enc/${archiveId}`}><EncIcon /></IconButton> + <IconButton color="primary" component={Link} to={`/enc/${id}`} disabled={disabled}><EncIcon /></IconButton> </MuiThemeProvider> </div> ) diff --git a/gui/src/components/Upload.js b/gui/src/components/Upload.js index b7ae4dd5da952e80ad14751418b1de9a61398145..c22d77d4ea8e66b77530a2bcfcee8b500e042baa 100644 --- a/gui/src/components/Upload.js +++ b/gui/src/components/Upload.js @@ -3,7 +3,9 @@ import PropTypes from 'prop-types' import { withStyles, ExpansionPanel, ExpansionPanelSummary, Typography, ExpansionPanelDetails, Stepper, Step, StepLabel, Table, TableRow, TableCell, TableBody, Checkbox, FormControlLabel, TablePagination, TableHead, Tooltip, - CircularProgress} from '@material-ui/core' + CircularProgress, + LinearProgress, + TableSortLabel} from '@material-ui/core' import ExpandMoreIcon from '@material-ui/icons/ExpandMore' import ReactJson from 'react-json-view' import CalcLinks from './CalcLinks' @@ -64,38 +66,56 @@ class Upload extends React.Component { state = { upload: this.props.upload, - page: 1, - rowsPerPage: 5 + params: { + page: 1, + perPage: 5, + orderBy: 'mainfile', + order: 'desc' + }, + loading: true, // its loading data from the server and the user should know about it + updating: true // it is still not complete and contineusly looking for updates } - updateUpload() { - window.setTimeout(() => { - this.state.upload.update() - .then(upload => { - console.assert(upload.proc, 'Uploads always must have a proc') - this.setState({upload: upload}) - if (upload.proc.status !== 'SUCCESS' && upload.proc.status !== 'FAILURE' && !upload.proc.is_stale) { - this.updateUpload() - } - }) - .catch(error => { - this.setState({upload: null}) - this.props.raiseError(error) - }) - }, 500) + update(params) { + const {page, perPage, orderBy, order} = params + this.setState({loading: true}) + this.state.upload.get(page, perPage, orderBy, order) + .then(upload => { + const continueUpdating = upload.status !== 'SUCCESS' && upload.status !== 'FAILURE' && !upload.is_stale + this.setState({upload: upload, loading: false, params: params, updating: continueUpdating}) + if (continueUpdating) { + window.setTimeout(() => { + if (!this.state.loading) { + this.update(this.state.params) + } + }, 500) + } + }) + .catch(error => { + this.setState({loading: false, ...params}) + this.props.raiseError(error) + }) } componentDidMount() { - this.updateUpload() + this.update(this.state.params) } handleChangePage = (_, page) => { - this.setState({page: page + 1}) + this.update({...this.state.params, page: page + 1}) } handleChangeRowsPerPage = event => { - const rowsPerPage = event.target.value - this.setState({rowsPerPage: rowsPerPage}) + const perPage = event.target.value + this.update({...this.state.params, perPage: perPage}) + } + + handleSort(orderBy) { + let order = 'desc' + if (this.state.params.orderBy === orderBy && this.state.params.order === 'desc') { + order = 'asc' + } + this.update({...this.state.params, orderBy: orderBy, order: order}) } onCheckboxChanged(_, checked) { @@ -123,16 +143,16 @@ class Upload extends React.Component { renderStepper() { const { classes } = this.props const { upload } = this.state - const { calc_procs, task_names, current_task_name, status, errors } = upload.proc + const { calcs, tasks, current_task, status, errors } = upload - let activeStep = task_names.indexOf(current_task_name) + let activeStep = tasks.indexOf(current_task) activeStep += (status === 'SUCCESS') ? 1 : 0 const labelPropsFactories = { uploading: (props) => { props.children = 'uploading' const { uploading } = upload - if (upload.proc.status !== 'FAILURE') { + if (upload.status !== 'FAILURE') { props.optional = ( <Typography variant="caption"> {uploading || 0}% @@ -142,7 +162,7 @@ class Upload extends React.Component { }, extracting: (props) => { props.children = 'extracting' - if (current_task_name === 'extracting') { + if (current_task === 'extracting') { props.optional = ( <Typography variant="caption"> be patient @@ -152,20 +172,20 @@ class Upload extends React.Component { }, parse_all: (props) => { props.children = 'parse' - if (calc_procs.length > 0) { - const failures = calc_procs.filter(calcProc => calcProc.status === 'FAILURE') - if (failures.length) { + if (calcs && calcs.pagination.total > 0) { + const { total, successes, failures } = calcs.pagination + + if (failures) { props.error = true props.optional = ( <Typography variant="caption" color="error"> - {calc_procs.filter(p => p.status === 'SUCCESS').length}/{calc_procs.length} - , {failures.length} failed + {successes + failures}/{total}, {failures} failed </Typography> ) } else { props.optional = ( <Typography variant="caption"> - {calc_procs.filter(p => p.status === 'SUCCESS').length}/{calc_procs.length} + {successes + failures}/{total} </Typography> ) } @@ -180,7 +200,7 @@ class Upload extends React.Component { return ( <Stepper activeStep={activeStep} classes={{root: classes.stepper}}> - {task_names.map((label, index) => { + {tasks.map((label, index) => { const labelProps = { children: label, error: activeStep === index && status === 'FAILURE' @@ -211,14 +231,15 @@ class Upload extends React.Component { renderCalcTable() { const { classes } = this.props - const { page, rowsPerPage } = this.state - const { calc_procs, status, upload_hash } = this.state.upload.proc + const { page, perPage, orderBy, order } = this.state.params + const { calcs, status } = this.state.upload + const { pagination, results } = calcs - if (calc_procs.length === 0) { - if (this.state.upload.is_ready) { + if (pagination.total === 0) { + if (this.state.upload.completed) { return ( <Typography className={classes.detailsContent}> - {status === 'SUCCESS' ? 'No calculcations found.' : 'There are errors and no calculations to show.'} + {status === 'SUCCESS' ? 'No calculcations found.' : 'No calculations to show.'} </Typography> ) } else { @@ -230,8 +251,8 @@ class Upload extends React.Component { } } - const renderRow = (calcProc, index) => { - const { mainfile, calc_hash, parser_name, task_names, current_task_name, status, errors } = calcProc + const renderRow = (calc, index) => { + const { mainfile, archive_id, parser, tasks, current_task, status, errors } = calc const color = status === 'FAILURE' ? 'error' : 'default' const row = ( <TableRow key={index}> @@ -240,28 +261,32 @@ class Upload extends React.Component { {mainfile} </Typography> <Typography variant="caption" color={color}> - {calc_hash} + {archive_id} </Typography> </TableCell> <TableCell> <Typography color={color}> - {parser_name.replace('parsers/', '')} + {parser.replace('parsers/', '')} </Typography> </TableCell> <TableCell> <Typography color={color}> - {current_task_name} + {current_task} </Typography> <Typography variant="caption" color={color}> task <b> - [{task_names.indexOf(current_task_name) + 1}/{task_names.length}] + [{tasks.indexOf(current_task) + 1}/{tasks.length}] </b> </Typography> </TableCell> <TableCell> - {status === 'SUCCESS' - ? <CalcLinks uploadHash={upload_hash} calcHash={calc_hash} /> : ''} + <Typography color={color}> + {status.toLowerCase()} + </Typography> + </TableCell> + <TableCell> + <CalcLinks calcId={archive_id} disabled={status !== 'SUCCESS'} /> </TableCell> </TableRow> ) @@ -277,21 +302,45 @@ class Upload extends React.Component { } } - const total = calc_procs.length - const emptyRows = rowsPerPage - Math.min(rowsPerPage, total - (page - 1) * rowsPerPage) + const total = pagination.total + const emptyRows = perPage - Math.min(perPage, total - (page - 1) * perPage) + + const columns = [ + { id: 'mainfile', sort: true, label: 'mainfile' }, + { id: 'parser', sort: true, label: 'code' }, + { id: 'task', sort: false, label: 'task' }, + { id: 'status', sort: true, label: 'status' }, + { id: 'links', sort: false, label: 'links' } + ] return ( <Table> <TableHead> <TableRow> - <TableCell>mainfile</TableCell> - <TableCell>code</TableCell> - <TableCell>task</TableCell> - <TableCell></TableCell> + {columns.map(column => ( + <TableCell key={column.id}> + {column.sort + ? <Tooltip + title="Sort" + placement={'bottom-start'} + enterDelay={300} + > + <TableSortLabel + active={orderBy === column.id} + direction={order} + onClick={() => this.handleSort(column.id)} + > + {column.label} + </TableSortLabel> + </Tooltip> + : column.label + } + </TableCell> + ))} </TableRow> </TableHead> <TableBody> - {calc_procs.slice((page - 1) * rowsPerPage, page * rowsPerPage).map(renderRow)} + {results.map(renderRow)} {emptyRows > 0 && ( <TableRow style={{ height: 57 * emptyRows }}> <TableCell colSpan={6} /> @@ -300,7 +349,7 @@ class Upload extends React.Component { <TableRow> <TablePagination count={total} - rowsPerPage={rowsPerPage} + rowsPerPage={perPage} page={page - 1} onChangePage={this.handleChangePage} onChangeRowsPerPage={this.handleChangeRowsPerPage} @@ -320,7 +369,7 @@ class Upload extends React.Component { <ExpansionPanel> <ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} classes={{root: classes.summary}}> - {!upload.is_ready + {!upload.completed ? <div className={classes.progress}> <CircularProgress size={32}/> </div> @@ -336,11 +385,12 @@ class Upload extends React.Component { {this.renderTitle()} {this.renderStepper()} </ExpansionPanelSummary> <ExpansionPanelDetails style={{width: '100%'}} classes={{root: classes.details}}> - {this.renderCalcTable()} + {upload.calcs ? this.renderCalcTable() : ''} {debug ? <div className={classes.detailsContent}> <ReactJson src={upload} enableClipboard={false} collapsed={0} /> </div> : ''} + {this.state.loading && !this.state.updating ? <LinearProgress/> : ''} </ExpansionPanelDetails> </ExpansionPanel> ) diff --git a/nomad/api.py b/nomad/api.py index a37904106daa2008998f24e05414bd97bfd94dd4..51868c6d26d14ea5cca77d84d119e10628fc707e 100644 --- a/nomad/api.py +++ b/nomad/api.py @@ -5,7 +5,7 @@ from elasticsearch.exceptions import NotFoundError from nomad import config, files from nomad.utils import get_logger, create_uuid -from nomad.processing import Upload, Calc, NotAllowedDuringProcessing +from nomad.processing import Upload, Calc, NotAllowedDuringProcessing, SUCCESS, FAILURE from nomad.repo import RepoCalc from nomad.user import me @@ -205,9 +205,13 @@ class UploadRes(Resource): except KeyError: abort(404, message='Upload with id %s does not exist.' % upload_id) - page = int(request.args.get('page', 1)) - per_page = int(request.args.get('per_page', 10)) - order_by = str(request.args.get('order_by', 'mainfile')) + try: + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 10)) + order_by = str(request.args.get('order_by', 'mainfile')) + order = int(str(request.args.get('order', -1))) + except Exception: + abort(400, message='invalid pagination or ordering') try: assert page >= 1 @@ -215,14 +219,20 @@ class UploadRes(Resource): except AssertionError: abort(400, message='invalid pagination') - if order_by not in ['mainfile', 'status']: + if order_by not in ['mainfile', 'status', 'parser']: abort(400, message='invalid order_by field %s' % order_by) + order_by = ('-%s' if order == -1 else '+%s') % order_by + all_calcs = Calc.objects(upload_id=upload_id) total = all_calcs.count() + successes = Calc.objects(upload_id=upload_id, status=SUCCESS).count() + failures = Calc.objects(upload_id=upload_id, status=FAILURE).count() calcs = all_calcs[(page - 1) * per_page:page * per_page].order_by(order_by) result['calcs'] = { - 'pagination': dict(total=total, page=page, per_page=per_page), + 'pagination': dict( + total=total, page=page, per_page=per_page, + successes=successes, failures=failures), 'results': [calc.json_dict for calc in calcs] } diff --git a/nomad/processing/__init__.py b/nomad/processing/__init__.py index 93eeac7dbaba4a8eb84ceb6d1d9e80371a6903c1..b2c9dc72425c1bd90d362b211829be36b3042ce9 100644 --- a/nomad/processing/__init__.py +++ b/nomad/processing/__init__.py @@ -75,6 +75,6 @@ Initiate processing """ -from nomad.processing.base import app, InvalidId, ProcNotRegistered +from nomad.processing.base import app, InvalidId, ProcNotRegistered, SUCCESS, FAILURE, RUNNING, PENDING from nomad.processing.data import Upload, Calc, NotAllowedDuringProcessing from nomad.processing.handler import handle_uploads, handle_uploads_thread diff --git a/nomad/processing/base.py b/nomad/processing/base.py index 6201f54b573dd1bb812456fb45bb91e246502ae3..bfa2eeb7ec2144fca66430ae9233d49ad6f8c77f 100644 --- a/nomad/processing/base.py +++ b/nomad/processing/base.py @@ -216,7 +216,7 @@ class Proc(Document, metaclass=ProcMetaclass): for warning in warnings: warning = str(warning) self.warnings.append(warning) - logger.log('task with warning', warning=warning, level=log_level) + Proc.log(logger, log_level, 'task with warning', warning=warning) def _continue_with(self, task): tasks = self.__class__.tasks diff --git a/nomad/processing/data.py b/nomad/processing/data.py index 2c95e566b90040e6eee9b02db0dd96178e34a928..aca6013cf6efcf3c05ee93dea1b802e14c596cd9 100644 --- a/nomad/processing/data.py +++ b/nomad/processing/data.py @@ -30,6 +30,7 @@ calculations, and files from typing import List, Any import sys from datetime import datetime +from elasticsearch.exceptions import NotFoundError from mongoengine import \ Document, EmailField, StringField, BooleanField, DateTimeField, \ ListField, DictField, ReferenceField, IntField, connect @@ -73,7 +74,7 @@ class Calc(Proc): meta: Any = { 'indices': [ - 'upload_id' + 'upload_id', 'mainfile', 'code', 'parser' ] } @@ -96,9 +97,12 @@ class Calc(Proc): files.delete_archive(self.archive_id) # delete the search index entry - elastic_entry = RepoCalc.get(self.archive_id) - if elastic_entry is not None: - elastic_entry.delete() + try: + elastic_entry = RepoCalc.get(self.archive_id) + if elastic_entry is not None: + elastic_entry.delete() + except NotFoundError: + pass # delete this mongo document super().delete() @@ -336,7 +340,7 @@ class Upload(Proc): calc.process() self.total_calcs += 1 except Exception as e: - self.warnings( + self.warning( 'exception while matching pot. mainfile', mainfile=filename, exc_info=e)