Commit 72079f2c authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Implemented Dataset page.

parent e789b95e
Pipeline #60303 passed with stages
in 31 minutes and 39 seconds
......@@ -22,13 +22,15 @@ import Calc from './entry/Calc'
import About from './About'
import LoginLogout from './LoginLogout'
import { genTheme, repoTheme, archiveTheme, appBase } from '../config'
import { DomainProvider } from './domains'
import { DomainProvider, withDomain } from './domains'
import {help as metainfoHelp, default as MetaInfoBrowser} from './metaInfoBrowser/MetaInfoBrowser'
import packageJson from '../../package.json'
import { Cookies, withCookies } from 'react-cookie'
import Markdown from './Markdown'
import {help as uploadHelp, default as Uploads} from './uploads/Uploads'
import ResolvePID from './entry/ResolvePID';
import ResolvePID from './entry/ResolvePID'
import DatasetPage from './DatasetPage'
import { capitalize } from '../utils'
export class VersionMismatch extends Error {
constructor(msg) {
......@@ -39,27 +41,7 @@ export class VersionMismatch extends Error {
const drawerWidth = 200
const toolbarTitles = {
'/': 'About, Documentation, Getting Help',
'/search': 'Find and Download Data',
'/uploads': 'Upload and Publish Data',
'/metainfo': 'The NOMAD Meta Info'
}
const toolbarThemes = {
'/': genTheme,
'/search': repoTheme,
'/uploads': repoTheme,
'/entry': repoTheme,
'/metainfo': archiveTheme
}
const toolbarHelp = {
'/': null,
'/search': {title: 'How to find and download data', content: searchHelp},
'/uploads': {title: 'How to upload data', content: uploadHelp},
'/metainfo': {title: 'About the NOMAD meta-info', content: metainfoHelp}
}
class NavigationUnstyled extends React.Component {
static propTypes = {
......@@ -177,6 +159,31 @@ class NavigationUnstyled extends React.Component {
}
}
toolbarTitles = {
'/': 'About, Documentation, Getting Help',
'/search': 'Find and Download Data',
'/uploads': 'Upload and Publish Data',
'/metainfo': 'The NOMAD Meta Info',
'/entry': capitalize(this.props.domain.entryLabel),
'/dataset': 'Dataset'
}
toolbarThemes = {
'/': genTheme,
'/search': repoTheme,
'/uploads': repoTheme,
'/entry': repoTheme,
'/dataset': repoTheme,
'/metainfo': archiveTheme
}
toolbarHelp = {
'/': null,
'/search': {title: 'How to find and download data', content: searchHelp},
'/uploads': {title: 'How to upload data', content: uploadHelp},
'/metainfo': {title: 'About the NOMAD meta-info', content: metainfoHelp}
}
componentDidMount() {
fetch(`${appBase}/meta.json`)
.then((response) => response.json())
......@@ -198,6 +205,7 @@ class NavigationUnstyled extends React.Component {
render() {
const { classes, children, location: { pathname }, loading } = this.props
const { toolbarThemes, toolbarHelp, toolbarTitles } = this
const selected = dct => {
const key = Object.keys(dct).find(key => {
......@@ -299,7 +307,7 @@ class NavigationUnstyled extends React.Component {
}
}
const Navigation = compose(withRouter, withErrors, withApi(false), withStyles(NavigationUnstyled.styles))(NavigationUnstyled)
const Navigation = compose(withRouter, withErrors, withApi(false), withDomain, withStyles(NavigationUnstyled.styles))(NavigationUnstyled)
class LicenseAgreementUnstyled extends React.Component {
static propTypes = {
......@@ -392,7 +400,7 @@ export default class App extends React.Component {
},
'entry': {
path: '/entry/id/:uploadId/:calcId',
key: (props) => `entry/${props.match.params.uploadId}/${props.match.params.uploadId}`,
key: (props) => `entry/id/${props.match.params.uploadId}/${props.match.params.uploadId}`,
render: props => {
const { match, ...rest } = props
if (match && match.params.uploadId && match.params.calcId) {
......@@ -402,6 +410,18 @@ export default class App extends React.Component {
}
}
},
'dataset': {
path: '/dataset/id/:datasetId',
key: (props) => `dataset/id/${props.match.params.datasetId}`,
render: props => {
const { match, ...rest } = props
if (match && match.params.datasetId) {
return (<DatasetPage {...rest} datasetId={match.params.datasetId} />)
} else {
return ''
}
}
},
'entry_pid': {
path: '/entry/pid/:pid',
key: (props) => `entry/pid/${props.match.params.pid}`,
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import { compose } from 'recompose'
import { withErrors } from './errors'
import { withApi } from './api'
import Search from './search/Search'
import { Typography, Link, Fab } from '@material-ui/core'
import Download from './entry/Download'
import DownloadIcon from '@material-ui/icons/CloudDownload'
export const help = `
This page allows you to **inspect** and **download** NOMAD datasets. It alsow allows you
to explore a dataset with similar controls that the search page offers.
`
class DatasetPage extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
datasetId: PropTypes.string.isRequired,
raiseError: PropTypes.func.isRequired
}
static styles = theme => ({
root: {
},
description: {
padding: theme.spacing.unit * 3
},
downloadFab: {
zIndex: 1,
right: 32,
top: 56 + 32,
position: 'fixed !important'
}
})
state = {
dataset: {}
}
componentDidMount() {
const {datasetId, raiseError, api} = this.props
api.search({
owner: 'all',
dataset_id: datasetId,
page: 1, per_page: 1
}).then(data => {
const entry = data.results[0]
const dataset = entry ? entry.datasets.find(ds => ds.id + '' === datasetId) : {}
this.setState({dataset: dataset || {}})
}).catch(error => {
this.setState({dataset: {}})
raiseError(error)
})
}
render() {
const { classes, datasetId } = this.props
const { dataset } = this.state
return (
<div className={classes.root}>
<div className={classes.description}>
<Typography variant="h4">{dataset.name || 'loading ...'}</Typography>
<Typography>
dataset{dataset.doi ? <span>, with DOI <Link href={dataset.doi}>{dataset.doi}</Link></span> : ''}
</Typography>
</div>
<Search searchParameters={{owner: 'all', dataset_id: datasetId}} />
<Download
classes={{root: classes.downloadFab}} tooltip="download all rawfiles"
component={Fab} className={classes.downloadFab} color="primary" size="medium"
url={`raw/query?dataset_id=${datasetId}`} fileName={`${dataset.name}.json`}
>
<DownloadIcon />
</Download>
</div>
)
}
}
export default compose(withApi(false), withErrors, withStyles(DatasetPage.styles))(DatasetPage)
......@@ -54,7 +54,7 @@ class DomainProviderBase extends React.Component {
still be missing when you are exploring Nomad data using the new search and data
exploring capabilities (menu items on the left).
`,
entryLabel: 'calculation',
entryLabel: 'code run',
searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values',
/**
* A component that is used to render the search aggregations. The components needs
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Divider, Card, CardContent, Grid, CardHeader, Fab, Typography } from '@material-ui/core'
import { withStyles, Divider, Card, CardContent, Grid, CardHeader, Fab, Typography, Link } from '@material-ui/core'
import { withApi } from '../api'
import { compose } from 'recompose'
import Download from './Download'
......@@ -8,6 +8,7 @@ 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'
class RepoEntryView extends React.Component {
static styles = theme => ({
......@@ -109,9 +110,11 @@ class RepoEntryView extends React.Component {
<Quantity column>
<Quantity quantity='comment' placeholder='no comment' {...quantityProps} />
<Quantity quantity='references' placeholder='no references' {...quantityProps}>
{(calcData.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
<div>
{(calcData.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
</div>
</Quantity>
<Quantity quantity='authors' {...quantityProps}>
<Typography>
......@@ -119,9 +122,13 @@ class RepoEntryView extends React.Component {
</Typography>
</Quantity>
<Quantity quantity='datasets' placeholder='no datasets' {...quantityProps}>
<Typography>
{(calcData.datasets || []).map(ds => `${ds.name}${ds.doi ? ` (${ds.doi})` : ''}`).join(', ')}
</Typography>
<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>
</Quantity>
</CardContent>
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import { IconButton, Typography, Divider, Tooltip, Tabs, Tab } from '@material-ui/core'
import { compose } from 'recompose'
import { withErrors } from '../errors'
import { withApi, DisableOnLoading } from '../api'
import SearchBar from './SearchBar'
import EntryList from './EntryList'
import SearchAggregations from './SearchAggregations'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import ExpandLessIcon from '@material-ui/icons/ExpandLess'
import { withDomain } from '../domains'
import DatasetList from './DatasetList';
import { isEquivalent } from '../../utils';
/**
* Component that comprises all search views: SearchBar, SearchAggregations (aka statistics),
* results (EntryList, DatasetList).
*/
class Search extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
domain: PropTypes.object,
loading: PropTypes.number,
searchParameters: PropTypes.object,
showDetails: PropTypes.bool
}
static styles = theme => ({
root: {
},
searchContainer: {
padding: theme.spacing.unit * 3
},
resultsContainer: {
},
searchEntry: {
minWidth: 500,
maxWidth: 900,
margin: 'auto',
width: '100%'
},
search: {
marginTop: theme.spacing.unit * 4,
marginBottom: theme.spacing.unit * 8,
display: 'flex',
alignItems: 'center',
minWidth: 500,
maxWidth: 1000,
margin: 'auto',
width: '100%'
},
searchBar: {
width: '100%'
},
searchDivider: {
width: 1,
height: 28,
margin: theme.spacing.unit * 0.5
},
searchButton: {
padding: 10
},
searchResults: {}
})
static emptySearchData = {
results: [],
pagination: {
total: 0
},
datasets: {
after: null,
values: []
},
statistics: {
total: {
all: {
datasets: 0
}
}
}
}
state = {
data: Search.emptySearchData,
searchState: {
...SearchAggregations.defaultState
},
entryListState: {
...EntryList.defaultState
},
datasetListState: {
...DatasetList.defaultState
},
showDetails: this.props.showDetails,
resultTab: 'entries'
}
constructor(props) {
super(props)
this.updateEntryList = this.updateEntryList.bind(this)
this.updateDatasetList = this.updateDatasetList.bind(this)
this.updateSearch = this.updateSearch.bind(this)
this.handleClickExpand = this.handleClickExpand.bind(this)
this._mounted = false
}
updateEntryList(changes) {
const entryListState = {
...this.state.entryListState, ...changes
}
this.update({entryListState: entryListState})
}
updateDatasetList(changes) {
const datasetListState = {
...this.state.datasetListState, ...changes
}
this.update({datasetListState: datasetListState})
}
updateSearch(changes) {
const searchState = {
...this.state.searchState, ...changes
}
this.update({searchState: searchState})
}
update(changes) {
if (!this._mounted) {
return
}
changes = changes || {}
const { searchParameters } = this.props
const { entryListState, datasetListState, searchState } = {...this.state, ...changes}
const { searchValues, ...searchStateRest } = searchState
this.setState({...changes})
this.props.api.search({
datasets: true,
statistics: true,
...entryListState,
...datasetListState,
...searchValues,
...searchStateRest,
...searchParameters
}).then(data => {
this.setState({
data: data || Search.emptySearchData
})
}).catch(error => {
if (error.name === 'NotAuthorized' && this.props.searchParameters.owner !== 'all') {
this.setState({data: Search.emptySearchData})
} else {
this.setState({data: Search.emptySearchData})
this.props.raiseError(error)
}
})
}
componentDidMount() {
this._mounted = true
this.update()
}
componentWillUnmount() {
this._mounted = false
}
componentDidUpdate(prevProps) {
// login/logout or changed search paraemters -> reload results
if (prevProps.api !== this.props.api || !isEquivalent(prevProps.searchParameters, this.props.searchParameters)) {
this.update()
}
}
handleClickExpand() {
this.setState({showDetails: !this.state.showDetails})
}
render() {
const { classes, domain, loading } = this.props
const { data, searchState, entryListState, datasetListState, showDetails, resultTab } = this.state
const { searchValues } = searchState
const { pagination: { total }, statistics } = data
const helperText = <span>
There are {Object.keys(domain.searchMetrics).filter(key => statistics.total.all[key]).map(key => {
return <span key={key}>
{domain.searchMetrics[key].renderResultString(!loading && statistics.total.all[key] !== undefined ? statistics.total.all[key] : '...')}
</span>
})}{Object.keys(searchValues).length ? ' left' : ''}.
</span>
return (
<div className={classes.root}>
<div className={classes.searchContainer}>
<DisableOnLoading>
<div className={classes.search}>
<SearchBar classes={{autosuggestRoot: classes.searchBar}}
fullWidth fullWidthInput={false} helperText={helperText}
label="search"
placeholder={domain.searchPlaceholder}
data={data} searchValues={searchValues}
InputLabelProps={{
shrink: true
}}
onChanged={values => this.updateSearch({searchValues: values})}
/>
<Divider className={classes.searchDivider} />
<Tooltip title={showDetails ? 'Hide statistics' : 'Show statistics'}>
<IconButton className={classes.searchButton} color="secondary" onClick={this.handleClickExpand}>
{showDetails ? <ExpandLessIcon/> : <ExpandMoreIcon/>}
</IconButton>
</Tooltip>
</div>
<div className={classes.searchEntry}>
<SearchAggregations
data={data} {...searchState} onChange={this.updateSearch}
showDetails={showDetails}
/>
</div>
</DisableOnLoading>
</div>
<div className={classes.resultsContainer}>
<Tabs
value={resultTab}
indicatorColor="primary"
textColor="primary"
onChange={(event, value) => this.setState({resultTab: value})}
>
<Tab label="Calculations" value="entries" />
<Tab label="Datasets" value="datasets" />
</Tabs>
<div className={classes.searchResults} hidden={resultTab !== 'entries'}>
<Typography variant="caption" style={{margin: 12}}>
About {total.toLocaleString()} results:
</Typography>
<EntryList
data={data} total={total}
onChange={this.updateEntryList}
{...entryListState}
/>
</div>
<div className={classes.searchResults} hidden={resultTab !== 'datasets'}>
<Typography variant="caption" style={{margin: 12}}>
About {statistics.total.all.datasets.toLocaleString()} datasets:
</Typography>
<DatasetList data={data} total={statistics.total.all.datasets}
onChange={this.updateDatasetList}
{...datasetListState}
/>
</div>
</div>
</div>
)
}
}
export default compose(withApi(false), withErrors, withDomain, withStyles(Search.styles))(Search)
......@@ -44,7 +44,10 @@ class SearchAggregationsUnstyled extends React.Component {
const firstRealQuantitiy = Object.keys(statistics).find(key => key !== 'total')
if (firstRealQuantitiy) {
const firstValue = Object.keys(statistics[firstRealQuantitiy])[0]
useMetric = Object.keys(statistics[firstRealQuantitiy][firstValue]).find(metric => metric !== 'code_runs') || 'code_runs'
if (firstValue) {
useMetric = Object.keys(statistics[firstRealQuantitiy][firstValue])
.find(metric => metric !== 'code_runs') || 'code_runs'
}
}
const metricsDefinitions = domain.searchMetrics
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import { FormControl, FormControlLabel, Checkbox, FormGroup,
FormLabel, IconButton, Typography, Divider, Tooltip, Tabs, Tab } from '@material-ui/core'
import { FormControl, FormControlLabel, Checkbox, FormGroup, FormLabel, Tooltip } from '@material-ui/core'
import { compose } from 'recompose'
import { withErrors } from '../errors'
import { withApi, DisableOnLoading } from '../api'
import SearchBar from './SearchBar'
import EntryList from './EntryList'
import SearchAggregations from './SearchAggregations'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import ExpandLessIcon from '@material-ui/icons/ExpandLess'
import { withDomain } from '../domains'
import { appBase } from '../../config'
import DatasetList from './DatasetList';
import Search from './Search';
export const help = `
This page allows you to **search** in NOMAD's data. The upper part of this page
......@@ -57,181 +50,26 @@ all parsed data. The *log* tab, will show you a log of the entry's processing.
class SearchPage extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
match: PropTypes.any,
api: PropTypes.object.isRequired,
user: PropTypes.object,
raiseError: PropTypes.func.isRequired,
domain: PropTypes.object,
loading: PropTypes.number
raiseError: PropTypes.func.isRequired
}
static styles = theme => ({
root: {
},
searchContainer: {
padding: theme.spacing.unit * 3
},