Commit 873efcae authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge branch 'v0.10.0' into 'master'

Merge v0.10.0 into master for release

Closes #475, #484, #497, #492, #498, and #500

See merge request !283
parents 5636e115 015742db
Pipeline #95934 passed with stage
in 2 minutes and 28 seconds
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import PropTypes from 'prop-types'
import Quantity from '../Quantity'
import { Divider, Grid, Card, CardHeader, CardContent, Typography, Link, makeStyles } from '@material-ui/core'
import { domains } from '../domains'
import ApiDialogButton from '../ApiDialogButton'
import { Link as RouterLink } from 'react-router-dom'
import { DOI } from '../search/DatasetList'
import { authorList } from '../../utils'
const useStyles = makeStyles(theme => ({
root: {
marginTop: theme.spacing(2)
},
error: {
marginTop: theme.spacing(2)
},
cardContent: {
paddingTop: 0
},
entryCards: {
marginTop: theme.spacing(2)
},
structureViewer: {
height: '25rem',
padding: '0'
}
}))
export default function DefaultEntryOverview({data, children}) {
const classes = useStyles()
const domain = data.domain && domains[data.domain]
let entryHeader = 'Entry metadata'
if (domain) {
entryHeader = domain.entryTitle(data)
}
return (
<Grid container spacing={2}>
<Grid item xs={7}>
<Card>
<CardHeader
title={entryHeader}
action={<ApiDialogButton title="Repository JSON" data={data} />}
/>
<CardContent classes={{root: classes.cardContent}}>
{children}
</CardContent>
<Divider />
<CardContent>
<Quantity column>
<Quantity quantity='comment' placeholder='no comment' data={data}/>
<Quantity quantity='references' placeholder='no references' data={data}>
{data.references &&
<div style={{display: 'inline-grid'}}>
{data.references.map(ref => <Typography key={ref} noWrap>
<Link href={ref}>{ref}</Link>
</Typography>)}
</div>}
</Quantity>
<Quantity quantity='authors' data={data}>
<Typography>
{authorList(data, true)}
</Typography>
</Quantity>
<Quantity quantity='datasets' data={data}>
{data.datasets && data.datasets.length > 0
? <div>
{data.datasets.map(ds => (
<Typography key={ds.dataset_id}>
<Link component={RouterLink} to={`/dataset/id/${ds.dataset_id}`}>{ds.name}</Link>
{ds.doi ? <span>&nbsp; (<DOI doi={ds.doi} />)</span> : ''}
</Typography>))}
</div>
: <Typography><i>not in any dataset</i></Typography>}
</Quantity>
<Quantity quantity='license' placeholder='unspecified' data={data}/>
</Quantity>
</CardContent>
</Card>
</Grid>
<Grid item xs={5}>
<Card>
<CardHeader title="Ids / processing" />
<CardContent classes={{root: classes.cardContent}}>
<Quantity column style={{maxWidth: 350}}>
<Quantity quantity="calc_id" label={`${domain ? domain.entryLabel : 'entry'} id`} noWrap withClipboard data={data} />
<Quantity quantity="encyclopedia.material.material_id" label='material id' noWrap data={data} withClipboard />
<Quantity quantity="mainfile" noWrap ellipsisFront data={data} withClipboard />
<Quantity quantity="upload_id" label='upload id' data={data} noWrap withClipboard />
<Quantity quantity="upload_time" label='upload time' noWrap data={data}>
<Typography noWrap>
{new Date(data.upload_time).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity="raw_id" label='raw id' noWrap hideIfUnavailable data={data} withClipboard />
<Quantity quantity="external_id" label='external id' hideIfUnavailable noWrap data={data} withClipboard />
<Quantity quantity="last_processing" label='last processing' placeholder="not processed" noWrap data={data}>
<Typography noWrap>
{new Date(data.last_processing).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity="last_processing" label='processing version' noWrap placeholder="not processed" dat={data}>
<Typography noWrap>
{data.nomad_version}/{data.nomad_commit}
</Typography>
</Quantity>
</Quantity>
</CardContent>
</Card>
</Grid>
</Grid>
)
}
DefaultEntryOverview.propTypes = {
data: PropTypes.object.isRequired,
children: PropTypes.object
}
......@@ -18,9 +18,10 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Tab, Tabs, Box } from '@material-ui/core'
import OverviewView from './OverviewView'
import ArchiveEntryView from './ArchiveEntryView'
import ArchiveLogView from './ArchiveLogView'
import RepoEntryView from './RepoEntryView'
import RawFileView from './RawFileView'
import KeepState from '../KeepState'
import { guiBase } from '../../config'
import { useRouteMatch, useHistory, Route } from 'react-router-dom'
......@@ -37,9 +38,11 @@ all parsed data.
The *log* tab, will show you a log of the entry's processing.
`
export function EntryPageContent({children, fixed}) {
const props = fixed ? {maxWidth: 1024} : {}
return <Box padding={3} margin="auto" {...props}>
export function EntryPageContent({children, width, minWidth, maxWidth}) {
width = width || '1200px'
minWidth = minWidth || '1200px'
maxWidth = maxWidth || '1200px'
return <Box boxSizing={'border-box'} width={width} minWidth={minWidth} maxWidth={maxWidth} padding={'1.5rem 2rem'} margin="auto">
{children}
</Box>
}
......@@ -47,8 +50,10 @@ EntryPageContent.propTypes = ({
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
fixed: PropTypes.bool
]),
width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
minWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
maxWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
})
export default function EntryPage() {
......@@ -58,24 +63,26 @@ export default function EntryPage() {
return (
<Route
path={`${path}/:uploadId?/:calcId?/:tab?`}
render={({match: {params: {uploadId, calcId, tab = 'raw'}}}) => {
render={({match: {params: {uploadId, calcId, tab = 'overview'}}}) => {
if (calcId && uploadId) {
const calcProps = { calcId: calcId, uploadId: uploadId }
return (
<React.Fragment>
<Tabs
value={tab || 'raw'}
value={tab || 'overview'}
onChange={(_, value) => history.push(`${url}/${uploadId}/${calcId}/${value}`)}
indicatorColor="primary"
textColor="primary"
variant="fullWidth"
>
<Tab label="Overview" value="overview" />
<Tab label="Raw data" value="raw" />
<Tab label="Archive" value="archive"/>
<Tab label="Logs" value="logs"/>
</Tabs>
<KeepState visible={tab === 'raw' || tab === undefined} render={props => <RepoEntryView {...props} />} {...calcProps} />
<KeepState visible={tab === 'overview' || tab === undefined} render={props => <OverviewView {...props} />} {...calcProps} />
<KeepState visible={tab === 'raw'} render={props => <RawFileView {...props} />} {...calcProps} />
<KeepState visible={tab === 'archive'} render={props => <ArchiveEntryView {...props} />} {...calcProps} />
<KeepState visible={tab === 'logs'} render={props => <ArchiveLogView {...props} />} {...calcProps} />
</React.Fragment>
......
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { apiContext } from '../api'
import { domains } from '../domains'
import { EntryPageContent } from './EntryPage'
import { errorContext } from '../errors'
import { Typography, makeStyles } from '@material-ui/core'
const useStyles = makeStyles(theme => ({
error: {
marginTop: theme.spacing(2)
},
cardContent: {
paddingTop: 0
},
topCard: {
height: '32rem'
},
toggle: {
marginBottom: theme.spacing(1)
},
structure: {
marginTop: theme.spacing(1),
width: '100%',
height: '20rem'
}
}))
/**
* Shows an informative overview about the selected entry.
*/
export default function OverviewView({uploadId, calcId}) {
const classes = useStyles()
const {api} = useContext(apiContext)
const {raiseError} = useContext(errorContext)
const [repo, setRepo] = useState(null)
const [exists, setExists] = useState(true)
// When loaded for the first time, download calc data from the ElasticSearch
// index. It is used quick to fetch and will be used to decide the subview to
// show.
useEffect(() => {
api.repo(uploadId, calcId).then(data => {
setRepo(data)
}).catch(error => {
if (error.name === 'DoesNotExist') {
setExists(false)
} else {
raiseError(error)
}
})
}, [api, raiseError, uploadId, calcId, setRepo, setExists])
// The entry does not exist
if (!exists) {
return <EntryPageContent>
<Typography className={classes.error}>
This entry does not exist.
</Typography>
</EntryPageContent>
}
// When repo data is loaded, return a subview that depends on the domain. If a
// specialized overview component has not been declared, a default component
// will be loaded.
if (repo) {
const domain = repo.domain && domains[repo.domain]
return <EntryPageContent fixed>
<domain.EntryOverview data={repo}/>
</EntryPageContent>
}
return null
}
OverviewView.propTypes = {
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired
}
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Typography, makeStyles } from '@material-ui/core'
import { apiContext } from '../api'
import { domains } from '../domains'
import { EntryPageContent } from './EntryPage'
import { errorContext } from '../errors'
const useStyles = makeStyles(theme => ({
error: {
marginTop: theme.spacing(2)
}
}))
export default function RawFileView({uploadId, calcId}) {
const classes = useStyles()
const {api} = useContext(apiContext)
const {raiseError} = useContext(errorContext)
const [state, setState] = useState({calcData: null, doesNotExist: false})
useEffect(() => {
setState({calcData: null, doesNotExist: false})
}, [setState, uploadId, calcId])
useEffect(() => {
api.repo(uploadId, calcId).then(data => {
setState({calcData: data, doesNotExist: false})
}).catch(error => {
if (error.name === 'DoesNotExist') {
setState({calcData: null, doesNotExist: true})
} else {
setState({calcData: null, doesNotExist: false})
raiseError(error)
}
})
}, [api, raiseError, uploadId, calcId, setState])
const calcData = state.calcData || {uploadId: uploadId, calcId: calcId}
const domain = calcData.domain && domains[calcData.domain]
if (state.doesNotExist) {
return <EntryPageContent>
<Typography className={classes.error}>
This entry does not exist.
</Typography>
</EntryPageContent>
}
return (
<EntryPageContent maxWidth={'1024px'} width={'100%'} minWidth={'800px'}>
{domain && <domain.EntryRawView data={calcData} calcId={calcId} uploadId={uploadId} />}
</EntryPageContent>
)
}
RawFileView.propTypes = {
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired
}
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { Divider, Card, CardContent, Grid, CardHeader, Typography, Link, makeStyles } from '@material-ui/core'
import { apiContext } from '../api'
import ApiDialogButton from '../ApiDialogButton'
// import Structure from '../visualization/Structure'
import Quantity from '../Quantity'
import { Link as RouterLink } from 'react-router-dom'
import { DOI } from '../search/DatasetList'
import { domains } from '../domains'
import { EntryPageContent } from './EntryPage'
import { errorContext } from '../errors'
import { authorList } from '../../utils'
const useStyles = makeStyles(theme => ({
root: {
marginTop: theme.spacing(2)
},
error: {
marginTop: theme.spacing(2)
},
cardContent: {
paddingTop: 0
},
entryCards: {
marginTop: theme.spacing(2)
},
structureViewer: {
height: '25rem',
padding: '0'
}
}))
export default function RepoEntryView({uploadId, calcId}) {
const classes = useStyles()
const {api} = useContext(apiContext)
const {raiseError} = useContext(errorContext)
const [state, setState] = useState({calcData: null, doesNotExist: false})
useEffect(() => {
setState({calcData: null, doesNotExist: false})
}, [setState, uploadId, calcId])
useEffect(() => {
api.repo(uploadId, calcId).then(data => {
setState({calcData: data, doesNotExist: false})
}).catch(error => {
if (error.name === 'DoesNotExist') {
setState({calcData: null, doesNotExist: true})
} else {
setState({calcData: null, doesNotExist: false})
raiseError(error)
}
})
}, [api, raiseError, uploadId, calcId, setState])
const calcData = state.calcData || {uploadId: uploadId, calcId: calcId}
const loading = !state.calcData
const quantityProps = {data: calcData, loading: loading}
const domain = calcData.domain && domains[calcData.domain]
let entryHeader = 'Entry metadata'
if (domain) {
entryHeader = domain.entryTitle(calcData)
}
if (state.doesNotExist) {
return <EntryPageContent className={classes.root} fixed>
<Typography className={classes.error}>
This entry does not exist.
</Typography>
</EntryPageContent>
}
return (
<EntryPageContent className={classes.root} fixed>
<Grid container spacing={2}>
<Grid item xs={7}>
<Card>
<CardHeader
title={entryHeader}
action={<ApiDialogButton title="Repository JSON" data={calcData} />}
/>
<CardContent classes={{root: classes.cardContent}}>
{domain && <domain.EntryOverview data={calcData} loading={loading} />}
</CardContent>
<Divider />
<CardContent>
<Quantity column>
<Quantity quantity='comment' placeholder='no comment' {...quantityProps} />
<Quantity quantity='references' placeholder='no references' {...quantityProps}>
{calcData.references &&
<div style={{display: 'inline-grid'}}>
{calcData.references.map(ref => <Typography key={ref} noWrap>
<Link href={ref}>{ref}</Link>
</Typography>)}
</div>}
</Quantity>
<Quantity quantity='authors' {...quantityProps}>
<Typography>
{authorList(loading ? null : calcData)}
</Typography>
</Quantity>
<Quantity quantity='datasets' placeholder='no datasets' {...quantityProps}>
{calcData.datasets &&
<div>
{calcData.datasets.map(ds => (
<Typography key={ds.dataset_id}>
<Link component={RouterLink} to={`/dataset/id/${ds.dataset_id}`}>{ds.name}</Link>
{ds.doi ? <span>&nbsp; (<DOI doi={ds.doi}/>)</span> : ''}
</Typography>))}
</div>}
</Quantity>
</Quantity>
</CardContent>
</Card>
</Grid>
<Grid item xs={5}>
<Card>
<CardHeader title="Ids / processing" />
<CardContent classes={{root: classes.cardContent}}>
<Quantity column style={{maxWidth: 350}}>
<Quantity quantity="calc_id" label={`${domain ? domain.entryLabel : 'entry'} id`} noWrap withClipboard {...quantityProps} />
<Quantity quantity="encyclopedia.material.material_id" label='material id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="mainfile" loading={loading} noWrap ellipsisFront {...quantityProps} withClipboard />
<Quantity quantity="upload_id" label='upload id' {...quantityProps} noWrap withClipboard />
<Quantity quantity="upload_time" label='upload time' noWrap {...quantityProps} >
<Typography noWrap>
{new Date(calcData.upload_time).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity="raw_id" label='raw id' loading={loading} noWrap hideIfUnavailable {...quantityProps} withClipboard />
<Quantity quantity="external_id" label='external id' loading={loading} hideIfUnavailable noWrap {...quantityProps} withClipboard />
<Quantity quantity="last_processing" label='last processing' loading={loading} placeholder="not processed" noWrap {...quantityProps}>
<Typography noWrap>
{new Date(calcData.last_processing).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity="last_processing" label='processing version' loading={loading} noWrap placeholder="not processed" {...quantityProps}>
<Typography noWrap>
{calcData.nomad_version}/{calcData.nomad_commit}
</Typography>
</Quantity>
</Quantity>
</CardContent>
</Card>
</Grid>
</Grid>
{domain && <domain.EntryCards data={calcData} calcId={calcId} uploadId={uploadId} classes={{root: classes.entryCards}} />}
</EntryPageContent>
)
}
RepoEntryView.propTypes = {
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired
}
......@@ -69,6 +69,8 @@ class ErrorSnacksUnstyled extends React.Component {
if (error instanceof Error) {
if (error.name === 'CannotReachApi') {
errorStr = 'Cannot reach NOMAD, please try again later.'
} else if (error.name === 'NotAuthorized') {
errorStr = error.message
} 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') {
......
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "Licens