Commit 05778e75 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge branch 'v0.7.0' into 'master'

V0.7.0

See merge request !71
parents 77b45967 769f314d
Pipeline #65673 passed with stage
in 27 seconds
......@@ -8,6 +8,19 @@ import { withApi, DoesNotExist } from '../api'
import { compose } from 'recompose'
import qs from 'qs'
import KeepState from '../KeepState'
import { guiBase } from '../../config'
export const help=`
The *raw files* tab, will show you all files that belong to the entry and offers a download
on individual, or all files. The files can be selected and downloaded. You can also
view the contents of some files directly here on this page.
The *archive* tab, shows you the parsed data as a tree
data structure. This view is connected to NOMAD's [meta-info](${guiBase}/metainfo), which acts a schema for
all parsed data.
The *log* tab, will show you a log of the entry's processing.
`
class EntryPage extends React.Component {
static styles = theme => ({
......
......@@ -6,6 +6,9 @@ import { withApi } from '../api'
import { compose } from 'recompose'
import Download from './Download'
import ReloadIcon from '@material-ui/icons/Cached'
import ViewIcon from '@material-ui/icons/Search'
import InfiniteScroll from 'react-infinite-scroller'
import { ScrollContext } from '../App'
class RawFiles extends React.Component {
static propTypes = {
......@@ -23,15 +26,58 @@ class RawFiles extends React.Component {
root: {},
formLabel: {
padding: theme.spacing.unit * 2
},
shownFile: {
color: theme.palette.secondary.main,
overflowX: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
direction: 'rtl',
textAlign: 'left'
},
fileContents: {
width: '85%',
overflowX: 'auto',
color: 'white',
background: '#222',
marginTop: 16,
padding: 8
},
fileError: {
marginTop: 16,
padding: 8
},
fileNameFormGroup: {
display: 'flex',
flexWrap: 'nowrap'
},
fileNameFormGroupLabel: {
flexGrow: 1,
overflowX: 'hidden',
marginRight: 0
},
fileNameLabel: {
overflowX: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
direction: 'rtl',
textAlign: 'left'
}
})
static defaultState = {
selectedFiles: [],
fileContents: null,
shownFile: null,
files: null,
doesNotExist: false
}
constructor(props) {
super(props)
this.handleFileClicked = this.handleFileClicked.bind(this)
}
state = {...RawFiles.defaultState}
componentDidUpdate(prevProps) {
......@@ -43,7 +89,7 @@ class RawFiles extends React.Component {
}
update() {
const { uploadId, calcId } = this.props
const { uploadId, calcId, raiseError } = 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) {
......@@ -52,13 +98,16 @@ class RawFiles extends React.Component {
this.props.api.getRawFileListFromCalc(uploadId, calcId).then(data => {
const files = data.contents.map(file => `${data.directory}/${file.name}`)
if (files.length > 500) {
raiseError('There are more than 500 files in this entry. We can only show the first 500.')
}
this.setState({files: files})
}).catch(error => {
this.setState({files: null})
if (error.name === 'DoesNotExist') {
this.setState({doesNotExist: true})
} else {
this.props.raiseError(error)
raiseError(error)
}
})
}
......@@ -67,7 +116,7 @@ class RawFiles extends React.Component {
return file.split('/').reverse()[0]
}
onSelectFile(file) {
handleSelectFile(file) {
const {selectedFiles} = this.state
const index = selectedFiles.indexOf(file)
if (index === -1) {
......@@ -78,9 +127,56 @@ class RawFiles extends React.Component {
}
}
handleFileClicked(file) {
const {api, uploadId, raiseError} = this.props
this.setState({shownFile: file, fileContents: null})
api.getRawFile(uploadId, file, {length: 16 * 1024})
.then(contents => this.setState({fileContents: contents}))
.catch(raiseError)
}
handleLoadMore(page) {
const {api, uploadId, calcId, raiseError} = this.props
const {fileContents, shownFile} = this.state
// The infinite scroll component has the issue if calling load more whenever it
// gets updates, therefore calling this infinitely before it gets any chances of
// receiving the results (https://github.com/CassetteRocks/react-infinite-scroller/issues/163).
// Therefore, we have to set hasMore to false first and set it to true again after
// receiving actual results.
this.setState({
fileContents: {
...fileContents,
hasMore: false
}
})
if (fileContents.contents.length < (page + 1) * 16 * 1024) {
api.getRawFile(uploadId, shownFile, {offset: page * 16 * 1024, length: 16 * 1024})
.then(contents => {
const {fileContents} = this.state
// The back-button navigation might cause a scroll event, might cause to loadmore,
// will set this state, after navigation back to this page, but potentially
// different entry.
if (this.props.calcId === calcId) {
this.setState({
fileContents: {
...contents,
contents: ((fileContents && fileContents.contents) || '') + contents.contents
}
})
}
})
.catch(error => {
this.setState({fileContents: null, shownFile: null})
raiseError(error)
})
}
}
render() {
const {classes, uploadId, calcId, loading, data} = this.props
const {selectedFiles, files, doesNotExist} = this.state
const {selectedFiles, files, doesNotExist, fileContents, shownFile} = this.state
const availableFiles = files || data.files || []
......@@ -117,6 +213,7 @@ class RawFiles extends React.Component {
{selectedFiles.length}/{availableFiles.length} files selected
</FormLabel>
<Download component={IconButton} disabled={selectedFiles.length === 0}
color="secondary"
tooltip="download selected files"
url={(selectedFiles.length === 1) ? `raw/${uploadId}/${selectedFiles[0]}` : `raw/${uploadId}?files=${encodeURIComponent(selectedFiles.join(','))}&strip=true`}
fileName={selectedFiles.length === 1 ? this.label(selectedFiles[0]) : `${calcId}.zip`}
......@@ -125,19 +222,60 @@ class RawFiles extends React.Component {
</Download>
</FormGroup>
<Divider />
<FormGroup row>
{availableFiles.map((file, index) => (
<FormControlLabel key={index} label={this.label(file)}
control={
<Checkbox
disabled={loading > 0}
checked={selectedFiles.indexOf(file) !== -1}
onChange={() => this.onSelectFile(file)} value={file}
/>
<div style={{display: 'flex', flexDirection: 'row'}}>
<div style={{width: '25%'}}>
{availableFiles.map((file, index) => (
<FormGroup row key={index} className={classes.fileNameFormGroup}>
<Tooltip title={file}>
<FormControlLabel
style={{flexGrow: 1, overflowX: 'hidden', textOverflow: 'ellipsis'}}
label={this.label(file)}
classes={{
root: classes.fileNameFormGroupLabel,
label: file === shownFile ? classes.shownFile : classes.fileNameLabel}}
control={
<Checkbox
disabled={loading > 0}
checked={selectedFiles.indexOf(file) !== -1}
onChange={() => this.handleSelectFile(file)} value={file}
/>}
/>
</Tooltip>
<Tooltip title='Show contents'>
<IconButton onClick={() => this.handleFileClicked(file)} color={file === shownFile ? 'secondary' : 'default'}>
<ViewIcon />
</IconButton>
</Tooltip>
</FormGroup>
))}
</div>
{fileContents && fileContents.contents !== null &&
<ScrollContext.Consumer>
{scroll =>
<InfiniteScroll
className={classes.fileContents}
pageStart={0}
loadMore={this.handleLoadMore.bind(this)}
hasMore={fileContents.hasMore}
useWindow={false}
getScrollParent={() => scroll.scrollParentRef}
>
<pre style={{margin: 0}}>
{`${fileContents.contents}`}
&nbsp;
</pre>
</InfiniteScroll>
}
/>
))}
</FormGroup>
</ScrollContext.Consumer>
}
{fileContents && fileContents.contents === null &&
<div className={classes.fileError}>
<Typography color="error">
Cannot display file due to unsupported file format.
</Typography>
</div>
}
</div>
</div>
)
}
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Divider, Card, CardContent, Grid, CardHeader, Fab, Typography, Link } from '@material-ui/core'
import { withStyles, Divider, Card, CardContent, Grid, CardHeader, Typography, Link } from '@material-ui/core'
import { withApi } from '../api'
import { compose } from 'recompose'
import Download from './Download'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import ApiDialogButton from '../ApiDialogButton'
import Quantity from '../Quantity'
import { withDomain } from '../domains'
import { Link as RouterLink } from 'react-router-dom'
import { DOI } from '../search/DatasetList'
class RepoEntryView extends React.Component {
static styles = theme => ({
root: {},
error: {
root: {
marginTop: theme.spacing.unit * 2
},
title: {
marginBottom: theme.spacing.unit * 3
},
content: {
marginTop: theme.spacing.unit * 3
},
downloadFab: {
zIndex: 1,
right: 32,
bottom: 32,
position: 'fixed !important'
error: {
marginTop: theme.spacing.unit * 2
},
cardContent: {
paddingTop: 0
},
entryCards: {
marginTop: theme.spacing.unit * 3
marginTop: theme.spacing.unit * 2
}
})
......@@ -86,9 +75,6 @@ class RepoEntryView extends React.Component {
const { uploadId, calcId } = calcProps
const quantityProps = {data: calcData, loading: loading}
const mainfile = calcData.mainfile
const calcPath = mainfile ? mainfile.substring(0, mainfile.lastIndexOf('/')) : null
const authors = loading ? null : calcData.authors
if (this.state.doesNotExist) {
......@@ -99,92 +85,80 @@ class RepoEntryView extends React.Component {
return (
<div className={classes.root}>
<div className={classes.content}>
<Grid container spacing={24}>
<Grid item xs={7}>
<Card>
<CardHeader
title="Metadata"
action={<ApiDialogButton title="Repository JSON" data={calcData} />}
/>
<CardContent classes={{root: classes.cardContent}}>
<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}>
<div>
{(calcData.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
</div>
</Quantity>
<Quantity quantity='authors' {...quantityProps}>
<Typography>
{(authors || []).map(author => author.name).join('; ')}
</Typography>
</Quantity>
<Quantity quantity='datasets' placeholder='no datasets' {...quantityProps}>
<div>
{(calcData.datasets || []).map(ds => (
<Typography key={ds.id}>
<Link component={RouterLink} to={`/dataset/id/${ds.id}`}>{ds.name}</Link>
{ds.doi ? <span>&nbsp; (<Link href={ds.doi}>{ds.doi}</Link>)</span> : ''}
</Typography>))}
</div>
</Quantity>
<Grid container spacing={24}>
<Grid item xs={7}>
<Card>
<CardHeader
title="Metadata"
action={<ApiDialogButton title="Repository JSON" data={calcData} />}
/>
<CardContent classes={{root: classes.cardContent}}>
<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}>
<div style={{display:'inline-grid'}}>
{(calcData.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
</div>
</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="pid" label='PID' loading={loading} placeholder="not yet assigned" noWrap {...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 * 1000).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity="calc_id" label={`${domain.entryLabel} id`} noWrap withClipboard {...quantityProps} />
<Quantity quantity='mainfile' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="calc_hash" label={`${domain.entryLabel} hash`} loading={loading} noWrap {...quantityProps} />
<Quantity quantity="raw_id" label='raw id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="external_id" label='external id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="last_processing" label='last processing' loading={loading} placeholder="not processed" noWrap {...quantityProps}>
<Typography noWrap>
{new Date(calcData.last_processing * 1000).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 quantity='authors' {...quantityProps}>
<Typography>
{(authors || []).map(author => author.name).join('; ')}
</Typography>
</Quantity>
<Quantity quantity='datasets' placeholder='no datasets' {...quantityProps}>
<div>
{(calcData.datasets || []).map(ds => (
<Typography key={ds.id}>
<Link component={RouterLink} to={`/dataset/id/${ds.id}`}>{ds.name}</Link>
{ds.doi ? <span>&nbsp; (<DOI doi={ds.doi}/>)</span> : ''}
</Typography>))}
</div>
</Quantity>
</CardContent>
</Card>
</Grid>
</Quantity>
</CardContent>
</Card>
</Grid>
<domain.EntryCards data={calcData} calcId={calcId} uploadId={uploadId} classes={{root: classes.entryCards}} />
<Download
disabled={!mainfile} tooltip="download all raw files for calculation"
classes={{root: classes.downloadFab}}
component={Fab} className={classes.downloadFab} color="primary" size="medium"
url={`raw/${uploadId}/${calcPath}/*?strip=true`} fileName={`${calcId}.zip`}
>
<DownloadIcon />
</Download>
<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.entryLabel} id`} noWrap withClipboard {...quantityProps} />
<Quantity quantity="pid" label='PID' loading={loading} placeholder="not yet assigned" noWrap {...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 * 1000).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity='mainfile' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="calc_hash" label={`${domain.entryLabel} hash`} loading={loading} noWrap {...quantityProps} />
<Quantity quantity="raw_id" label='raw id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="external_id" label='external id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="last_processing" label='last processing' loading={loading} placeholder="not processed" noWrap {...quantityProps}>
<Typography noWrap>
{new Date(calcData.last_processing * 1000).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>
</div>
<domain.EntryCards data={calcData} calcId={calcId} uploadId={uploadId} classes={{root: classes.entryCards}} />
</div>
)
}
......
......@@ -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, please try again later.'
errorStr = 'Cannot reach 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') {
......
......@@ -86,7 +86,7 @@ const ITEM_HEIGHT = 48
const styles = theme => ({
root: {
width: 300
width: 450
},
chip: {
margin: theme.spacing.unit / 4
......
......@@ -45,7 +45,7 @@ const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 300
width: 300, maxHeight: '90vh'
}
}
}
......@@ -69,7 +69,8 @@ class MetaInfoBrowser extends Component {
width: 300
},
search: {
marginLeft: theme.spacing.unit * 3
width: 450,
marginRight: theme.spacing.unit * 2
}
})
......@@ -161,6 +162,11 @@ class MetaInfoBrowser extends Component {
return <div>
<div className={classes.forms}>
<form style={{ display: 'flex' }}>
<MetainfoSearch
classes={{container: classes.search}}
suggestions={Object.values(metainfos.names).filter(metainfo => !schema.isPackage(metainfo))}
onChange={this.handleSearch}
/>
<FormControl disabled={loading > 0}>
<InputLabel htmlFor="select-multiple-checkbox">Package</InputLabel>
<Select
......@@ -180,10 +186,6 @@ class MetaInfoBrowser extends Component {
}
</Select>
</FormControl>
<MetainfoSearch classes={{root: classes.search}}
suggestions={Object.values(metainfos.names).filter(metainfo => !schema.isPackage(metainfo))}
onChange={this.handleSearch}
/>
</form>
</div>
<Viewer key={loadedPackage} rootElement={metainfo} packages={metainfos.contents} />
......
import React from 'react'
import PropTypes from 'prop-types'
import deburr from 'lodash/deburr'
import Autosuggest from 'react-autosuggest'
import match from 'autosuggest-highlight/match'
import parse from 'autosuggest-highlight/parse'
import TextField from '@material-ui/core/TextField'
import Paper from '@material-ui/core/Paper'
......@@ -30,23 +28,18 @@ function renderInputComponent(inputProps) {
}
function renderSuggestion(suggestion, { query, isHighlighted }) {
const matches = match(suggestion.name, query)
const inputValue = query.trim()
const matches = match(suggestion.name, inputValue)
const parts = parse(suggestion.name, matches)
return (
<MenuItem selected={isHighlighted} component="div">
<div>
{parts.map((part, index) =>
part.highlight ? (
<span key={String(index)} style={{ fontWeight: 500 }}>
{part.text}
</span>
) : (
<strong key={String(index)} style={{ fontWeight: 300 }}>
{part.text}
</strong>
)
)}
{parts.map((part, i) => (
<span key={i} style={{ fontWeight: part.highlight ? 500 : 300 }}>
{part.text}
</span>
))}