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

Merge branch 'v0.6.2' into 'master'

v0.6.2

See merge request !65
parents 1278f80f 93dd8040
Pipeline #62815 passed with stages
in 22 minutes
......@@ -31,8 +31,8 @@ import {help as uploadHelp, default as Uploads} from './uploads/Uploads'
import ResolvePID from './entry/ResolvePID'
import DatasetPage from './DatasetPage'
import { capitalize } from '../utils'
import { makeStyles } from '@material-ui/core/styles'
import { amber } from '@material-ui/core/colors'
import KeepState from './KeepState'
export class VersionMismatch extends Error {
constructor(msg) {
......@@ -471,6 +471,7 @@ export default class App extends React.Component {
'metainfo': {
exact: true,
path: '/metainfo',
singleton: true,
render: props => <MetaInfoBrowser {...props} />
},
'metainfoEntry': {
......@@ -481,21 +482,13 @@ export default class App extends React.Component {
}
renderChildren(routeKey, props) {
// const { match, ...rest } = props
return (
<div>
{Object.keys(this.routes)
.filter(route => this.routes[route].singleton || route === routeKey)
.map(route => (
<div
key={route.key ? route.key(props) : route}
style={{display: routeKey === route ? 'block' : 'none'}}
>
{this.routes[route].render(props)}
</div>
))}
</div>
<React.Fragment>
{Object.keys(this.routes).map(route => <KeepState key={route}
visible={routeKey === route}
render={(props) => this.routes[route].render(props)}
{...props} />)}
</React.Fragment>
)
}
......
......@@ -40,7 +40,7 @@ class DatasetPage extends React.Component {
dataset: {}
}
componentDidMount() {
update() {
const {datasetId, raiseError, api} = this.props
api.search({
owner: 'all',
......@@ -56,6 +56,16 @@ class DatasetPage extends React.Component {
})
}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.api !== this.props.api || prevProps.datasetId !== this.props.datasetId) {
this.update()
}
}
render() {
const { classes, datasetId } = this.props
const { dataset } = this.state
......
import React from 'react'
import PropTypes from 'prop-types'
/**
* This is a kinda-HOC that allows to keep a component alive while not being visible.
*/
export default class KeepState extends React.Component {
static propTypes = {
visible: PropTypes.bool,
render: PropTypes.func.isRequired
}
state = {
props: null
}
update() {
const { visible, render, ...props } = this.props
if (this.props.visible) {
this.setState({props: props})
}
}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.visible !== this.props.visible && this.props.visible) {
this.update()
}
}
render() {
const { visible, render, ...other } = this.props
const props = visible ? other : this.state.props
if (props) {
return <div style={{display: visible ? 'block' : 'none'}}>
{render(props)}
</div>
} else {
return ''
}
}
}
\ No newline at end of file
......@@ -120,11 +120,11 @@ function handleApiError(e) {
throw e
}
let error = null
if (e.response) {
const body = e.response.body
const message = (body && body.message) ? body.message : e.response.statusText
const errorMessage = `${message} (${e.response.status})`
let error = null
if (e.response.status === 404) {
error = new DoesNotExist(errorMessage)
} else if (e.response.status === 401) {
......@@ -135,11 +135,16 @@ function handleApiError(e) {
error = new Error(errorMessage)
}
error.status = e.response.status
throw error
} else {
const errorMessage = e.status ? `${e} (${e.status})` : '' + e
throw new Error(errorMessage)
if (e.message === 'Failed to fetch') {
error = new ApiError(e.message)
error.status = 400
} else {
const errorMessage = e.status ? `${e} (${e.status})` : '' + e
error = new Error(errorMessage)
}
}
throw error
}
class Api {
......
......@@ -5,9 +5,7 @@ import RawFiles from '../entry/RawFiles'
class DFTEntryCards extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
loading: PropTypes.bool
classes: PropTypes.object.isRequired
}
static styles = theme => ({
......@@ -15,13 +13,13 @@ class DFTEntryCards extends React.Component {
})
render() {
const { classes, data } = this.props
const { classes, ...props } = this.props
return (
<Card className={classes.root}>
<CardHeader title="Raw files" />
<CardContent classes={{root: classes.cardContent}}>
<RawFiles data={data} />
<RawFiles {...props} />
</CardContent>
</Card>
)
......
......@@ -63,16 +63,21 @@ class ArchiveEntryView extends React.Component {
bottom: 32,
position: 'fixed !important'
}
});
})
static defaultState = {
data: null,
showMetaInfo: false,
doesNotExist: false
}
state = {
metaInfo: null,
...ArchiveEntryView.defaultState
}
constructor(props) {
super(props)
this.state = {
data: null,
metaInfo: null,
showMetaInfo: false,
doesNotExist: false
}
this.unmounted = false
}
......@@ -87,13 +92,19 @@ class ArchiveEntryView extends React.Component {
componentDidUpdate(prevProps) {
if (prevProps.api !== this.props.api || prevProps.info !== this.props.info) {
this.updateArchive()
this.updateMetaInfo()
}
if (prevProps.api !== this.props.api ||
prevProps.uploadId !== this.props.uploadId ||
prevProps.calcId !== this.props.calcId) {
this.setState({...ArchiveEntryView.defaultState})
this.updateArchive()
}
}
updateMetaInfo() {
if (this.props.api && this.props.info) {
if (this.props.api && this.props.info && !this.state.metaInfo) {
this.props.api.getMetaInfo(this.props.info.domain.metainfo.all_package).then(metaInfo => {
if (!this.unmounted) {
this.setState({metaInfo: metaInfo})
......
......@@ -32,20 +32,22 @@ class ArchiveLogView extends React.Component {
}
});
constructor(props) {
super(props)
this.state = {
static defaultState = {
data: null,
doesNotExist: false
}
}
state = {...ArchiveLogView.defaultState}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.api !== this.props.api) {
if (prevProps.api !== this.props.api ||
prevProps.uploadId !== this.props.uploadId ||
prevProps.calcId !== this.props.calcId) {
this.setState({...ArchiveLogView.defaultState})
this.update()
}
}
......
......@@ -7,6 +7,7 @@ import RepoEntryView from './RepoEntryView'
import { withApi, DoesNotExist } from '../api'
import { compose } from 'recompose'
import qs from 'qs'
import KeepState from '../KeepState'
class EntryPage extends React.Component {
static styles = theme => ({
......@@ -22,28 +23,33 @@ class EntryPage extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
uploadId: PropTypes.string,
calcId: PropTypes.string,
location: PropTypes.object,
query: PropTypes.bool
}
state = {
static defaultState = {
viewIndex: 0,
calcId: null,
uploadId: null
}
state = {...EntryPage.defaultState}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.query !== this.props.query
|| prevProps.location !== this.props.location
|| prevProps.uploadId !== this.props.uploadId
|| prevProps.calcId !== this.props.calcId
|| prevProps.api !== this.props.api) {
if (prevProps.query !== this.props.query ||
prevProps.location.key !== this.props.location.key ||
prevProps.uploadId !== this.props.uploadId ||
prevProps.calcId !== this.props.calcId ||
prevProps.query !== this.props.query ||
prevProps.api !== this.props.api) {
this.setState({ ...EntryPage.defaultState })
this.update()
}
}
......@@ -58,14 +64,14 @@ class EntryPage extends React.Component {
this.props.api.search({...queryParams}).then(data => {
if (data.results && data.results.length > 0) {
const { calc_id, upload_id } = data.results[0]
this.setState({uploadId: upload_id, calcId: calc_id})
this.setState({uploadId: upload_id, calcId: calc_id, viewIndex: 0})
} else {
this.props.raiseError(new DoesNotExist())
}
}).catch(this.props.raiseError)
} else {
if (calcId && uploadId) {
this.setState({calcId: calcId, uploadId: uploadId})
this.setState({calcId: calcId, uploadId: uploadId, viewIndex: 0})
} else {
// this should be unreachable
this.props.raiseError(new DoesNotExist())
......@@ -95,15 +101,9 @@ class EntryPage extends React.Component {
</Tabs>
<div className={classes.content}>
<div style={viewIndex !== 0 ? {display: 'none'} : {}} >
<RepoEntryView {...calcProps} />
</div>
<div style={viewIndex !== 1 ? {display: 'none'} : {}} >
<ArchiveEntryView {...calcProps} />
</div>
<div style={viewIndex !== 2 ? {display: 'none'} : {}} >
<ArchiveLogView {...calcProps} />
</div>
<KeepState visible={viewIndex === 0} render={props => <RepoEntryView {...props} />} {...calcProps} />
<KeepState visible={viewIndex === 1} render={props => <ArchiveEntryView {...props} />} {...calcProps} />
<KeepState visible={viewIndex === 2} render={props => <ArchiveLogView {...props} />} {...calcProps} />
</div>
</div>
)
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, FormGroup, FormControlLabel, Checkbox, FormLabel, IconButton, Divider, Typography } from '@material-ui/core'
import { withStyles, FormGroup, FormControlLabel, Checkbox, FormLabel, IconButton, Divider, Typography, Tooltip } from '@material-ui/core'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import { withApi } from '../api'
import { compose } from 'recompose'
import Download from './Download'
import ReloadIcon from '@material-ui/icons/Cached'
class RawFiles extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired,
data: PropTypes.object,
api: PropTypes.object.isRequired,
user: PropTypes.object,
loading: PropTypes.number.isRequired,
......@@ -23,25 +26,25 @@ class RawFiles extends React.Component {
}
})
state = {
static defaultState = {
selectedFiles: [],
uploadDirectory: null,
files: null,
doesNotExist: false
}
componentDidMount() {
this.update()
}
state = {...RawFiles.defaultState}
componentDidUpdate(prevProps) {
if (prevProps.api !== this.props.api) {
this.update()
if (prevProps.api !== this.props.api ||
prevProps.uploadId !== this.props.uploadId ||
prevProps.calcId !== this.props.calcId) {
this.setState({...RawFiles.defaultState})
}
}
update() {
const {data: {uploadId, calcId}} = this.props
const { uploadId, calcId } = this.props
// this might accidentally happen, when the user logs out and the ids aren't
// necessarily available anymore, but the component is still mounted
if (!uploadId || !calcId) {
......@@ -76,10 +79,10 @@ class RawFiles extends React.Component {
}
render() {
const {classes, data: {upload_id, calc_id}, loading} = this.props
const {classes, uploadId, calcId, loading, data} = this.props
const {selectedFiles, files, uploadDirectory, doesNotExist} = this.state
const availableFiles = files ? files.map(file => file.name) : []
const availableFiles = files ? files.map(file => file.name) : data.files || []
const someSelected = selectedFiles.length > 0
const allSelected = availableFiles.length === selectedFiles.length && someSelected
......@@ -103,13 +106,20 @@ class RawFiles extends React.Component {
/>
}
/>
{!files
? <Tooltip title="check for more files">
<IconButton onClick={() => this.update()}>
<ReloadIcon />
</IconButton>
</Tooltip> : ''
}
<FormLabel className={classes.formLabel}>
{selectedFiles.length}/{availableFiles.length} files selected
</FormLabel>
<Download component={IconButton} disabled={selectedFiles.length === 0}
tooltip="download selected files"
url={(selectedFiles.length === 1) ? `raw/${upload_id}/${uploadDirectory}/${selectedFiles[0]}` : `raw/${upload_id}?files=${encodeURIComponent(selectedFiles.map(file => `${uploadDirectory}/${file}`).join(','))}`}
fileName={selectedFiles.length === 1 ? this.label(selectedFiles[0]) : `${calc_id}.zip`}
url={(selectedFiles.length === 1) ? `raw/${uploadId}/${uploadDirectory}/${selectedFiles[0]}` : `raw/${uploadId}?files=${encodeURIComponent(selectedFiles.map(file => `${uploadDirectory}/${file}`).join(','))}`}
fileName={selectedFiles.length === 1 ? this.label(selectedFiles[0]) : `${calcId}.zip`}
>
<DownloadIcon />
</Download>
......
......@@ -45,17 +45,22 @@ class RepoEntryView extends React.Component {
domain: PropTypes.object.isRequired
}
state = {
static defaultState = {
calcData: null,
doesNotExist: false
}
state = {...RepoEntryView.defaultState}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.api !== this.props.api) {
if (prevProps.api !== this.props.api ||
prevProps.uploadId !== this.props.uploadId ||
prevProps.calcId !== this.props.calcId) {
this.setState({...RepoEntryView.defaultState})
this.update()
}
}
......@@ -168,7 +173,7 @@ class RepoEntryView extends React.Component {
</Grid>
</Grid>
<domain.EntryCards data={calcData} classes={{root: classes.entryCards}} />
<domain.EntryCards data={calcData} calcId={calcId} uploadId={uploadId} classes={{root: classes.entryCards}} />
<Download
disabled={!mainfile} tooltip="download all raw files for calculation"
......
......@@ -5,7 +5,7 @@ import { compose } from 'recompose'
import { withApi } from '../api'
import { withRouter } from 'react-router'
class ResovlePID extends React.Component {
class ResolvePID extends React.Component {
static styles = theme => ({
root: {
padding: theme.spacing.unit * 3
......@@ -20,11 +20,13 @@ class ResovlePID extends React.Component {
raiseError: PropTypes.func.isRequired
}
state = {
static defaultState = {
doesNotExist: false
}
componentDidMount() {
state = {...ResolvePID.defaultState}
update() {
const { pid, api, history } = this.props
api.resolvePid(pid).then(entry => {
history.push(`/entry/id/${entry.upload_id}/${entry.calc_id}`)
......@@ -38,6 +40,17 @@ class ResovlePID extends React.Component {
})
}
componentDidMount() {
this.update()
}
componentDidUpdate(prevProps) {
if (prevProps.pid !== this.props.pid || prevProps.api !== this.props.api) {
this.setState({...ResolvePID.defaultState})
this.update()
}
}
render() {
const { classes, api } = this.props
const { doesNotExist } = this.state
......@@ -62,4 +75,4 @@ class ResovlePID extends React.Component {
}
}
export default compose(withRouter, withApi(false), withStyles(ResovlePID.styles))(ResovlePID)
export default compose(withRouter, withApi(false), withStyles(ResolvePID.styles))(ResolvePID)
......@@ -44,7 +44,7 @@ class ErrorSnacksUnstyled extends React.Component {
let errorStr = 'Unexpected error. Please try again and let us know, if this error keeps happening.'
if (error instanceof Error) {
if (error.name === 'CannotReachApi') {
errorStr = 'Cannot reach the NOMAD API, please try again later.'
errorStr = 'Cannot reach the NOMAD, please try again later.'
} else if (error.name === 'DoesNotExist') {
errorStr = 'You are trying to access information that does not exist. Please try again and let us know, if this error keeps happening.'
} else if (error.name === 'VersionMismatch') {
......
......@@ -17,7 +17,6 @@ export class Definition extends React.Component {
} else if (schema.isProperty(definition)) {
return <ValueCard {...this.props}/>
} else {
console.log(definition)
return <div>{definition.mType}</div>
}
}
......
......@@ -183,7 +183,8 @@ class CardCompartmentUnstyled extends React.Component {
},
minHeader: {
position: 'relative',
minHeight: 16 + theme.spacing.unit
minHeight: 16 + theme.spacing.unit,
overflowY: 'hidden'
},
foldButton: {
position: 'absolute',
......
......@@ -66,7 +66,6 @@ class QuantityHistogram extends React.Component {
const selected = this.props.value
const width = this.container.current.offsetWidth
console.log('B ' + width)
const height = Object.keys(this.props.data).length * 32
const data = Object.keys(this.props.data)
......
......@@ -70,6 +70,7 @@ class ArchiveCalcLogResource(Resource):
upload_files.archive_log_file(calc_id, 'rb'),
mimetype='text/plain',
as_attachment=True,
cache_timeout=0,
attachment_filename='%s.log' % archive_id)
except Restricted:
abort(401, message='Not authorized to access %s/%s.' % (upload_id, calc_id))
......@@ -105,6 +106,7 @@ class ArchiveCalcResource(Resource):
upload_file.archive_file(calc_id, 'rb'),
mimetype='application/json',
as_attachment=True,
cache_timeout=0,
attachment_filename='%s.json' % archive_id)
except Restricted:
abort(401, message='Not authorized to access %s/%s.' % (upload_id, calc_id))
......
......@@ -25,6 +25,7 @@ import magic
import sys
import contextlib
import fnmatch
import json
from nomad import search, utils
from nomad.files import UploadFiles, Restricted
......@@ -148,6 +149,7 @@ def get_raw_file_from_upload_path(
raw_file_view,
mimetype=mime_type,
as_attachment=True,
cache_timeout=0,
attachment_filename=os.path.basename(upload_filepath))
except Restricted:
abort(401, message='Not authorized to access all files in %s.' % upload_files.upload_id)
......@@ -180,8 +182,7 @@ class RawFileFromUploadPathResource(Resource):
@login_if_available
@with_signature_token
def get(self, upload_id: str, path: str):
"""
Get a single raw calculation file, directory contents, or whole directory sub-tree
""" Get a single raw calculation file, directory contents, or whole directory sub-tree
from a given upload.