Scheduled maintenance on Monday 2019-06-24 between 10:00-11:00 CEST

...
 
Commits (17)
......@@ -20,86 +20,85 @@
branch = nomad-fair
[submodule "dependencies/parsers/cp2k"]
path = dependencies/parsers/cp2k
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-cp2k
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-cp2k.git
branch = nomad-fair
[submodule "dependencies/parsers/crystal"]
path = dependencies/parsers/crystal
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-crystal
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-crystal.git
branch = nomad-fair
[submodule "dependencies/parsers/cpmd"]
path = dependencies/parsers/cpmd
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-cpmd
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-cpmd.git
branch = nomad-fair
[submodule "dependencies/parsers/nwchem"]
path = dependencies/parsers/nwchem
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-nwchem
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-nwchem.git
branch = nomad-fair
[submodule "dependencies/parsers/bigdft"]
path = dependencies/parsers/bigdft
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-big-dft
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-big-dft.git
branch = nomad-fair
[submodule "dependencies/parsers/wien2k"]
path = dependencies/parsers/wien2k
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-wien2k
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-wien2k.git
branch = nomad-fair
[submodule "dependencies/parsers/band"]
path = dependencies/parsers/band
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-band
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-band.git
[submodule "dependencies/parsers/gaussian"]
path = dependencies/parsers/gaussian
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gaussian
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gaussian.git
[submodule "dependencies/parsers/quantum-espresso"]
path = dependencies/parsers/quantum-espresso
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-quantum-espresso
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-quantum-espresso.git
[submodule "dependencies/parsers/abinit"]
path = dependencies/parsers/abinit
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-abinit
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-abinit.git
[submodule "dependencies/parsers/orca"]
path = dependencies/parsers/orca
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-orca
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-orca.git
[submodule "dependencies/parsers/castep"]
path = dependencies/parsers/castep
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-castep
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-castep.git
[submodule "dependencies/parsers/dl-poly"]
path = dependencies/parsers/dl-poly
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-dl-poly
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-dl-poly.git
[submodule "dependencies/parsers/lib-atoms"]
path = dependencies/parsers/lib-atoms
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-lib-atoms
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-lib-atoms.git
[submodule "dependencies/parsers/octopus"]
path = dependencies/parsers/octopus
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-octopus
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-octopus.git
[submodule "dependencies/parsers/phonopy"]
path = dependencies/parsers/phonopy
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-phonopy
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-phonopy.git
[submodule "dependencies/parsers/phonopy-library"]
path = dependencies/parsers/phonopy-library
url = https://gitlab.mpcdf.mpg.de/nomad-lab/phonopy
url = https://gitlab.mpcdf.mpg.de/nomad-lab/phonopy.git
[submodule "dependencies/parsers/gpaw"]
path = dependencies/parsers/gpaw
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gpaw
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gpaw.git
[submodule "dependencies/parsers/atk"]
path = dependencies/parsers/atk
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-atk
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-atk.git
[submodule "dependencies/parsers/gulp"]
path = dependencies/parsers/gulp
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gulp
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gulp.git
[submodule "dependencies/parsers/siesta"]
path = dependencies/parsers/siesta
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-siesta
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-siesta.git
[submodule "dependencies/parsers/elk"]
path = dependencies/parsers/elk
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-elk
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-elk.git
[submodule "dependencies/parsers/elastic"]
path = dependencies/parsers/elastic
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-elastic
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-elastic.git
[submodule "dependencies/parsers/gamess"]
path = dependencies/parsers/gamess
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gamess
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-gamess.git
[submodule "dependencies/parsers/turbomole"]
path = dependencies/parsers/turbomole
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-turbomole
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-turbomole.git
[submodule "dependencies/parsers/photoemission"]
path = dependencies/parsers/photoemission
url = https://gitlab.mpcdf.mpg.de/nomad-lab/parser-photoemission.git
......
......@@ -36,6 +36,8 @@ source .pyenv/bin/activate
Third, install the development dependencies, including the documentation system
[sphinx](http://www.sphinx-doc.org/en/master/index.html):
```
pip install --upgrade pip
pip install --upgrade setuptools
pip install -r requirements.txt
```
......
Subproject commit 0d50b13f66f45891bc441a82af73bb425167ee71
Subproject commit 418ea7a2fae3c5726e970f52574916955ffe2fe4
Subproject commit 9056d2ac9586ee12f05fa76cdb8b09cc422c4af1
Subproject commit 6744335e87c6ec60190c092eedc4c12d54baae17
......@@ -21,6 +21,7 @@ import Calc from './entry/Calc'
import About from './About'
import LoginLogout from './LoginLogout'
import { genTheme, repoTheme } from '../config'
import { DomainProvider } from './domains'
const drawerWidth = 200
......@@ -259,16 +260,19 @@ export default class App extends React.Component {
routes = {
'about': {
exact: true,
singleton: true,
path: '/',
render: props => <About {...props} />
},
'search': {
exact: true,
singleton: true,
path: '/search',
render: props => <SearchPage {...props} />
},
'searchEntry': {
path: '/search/:uploadId/:calcId',
key: (props) => `searchEntry/${props.match.params.uploadId}/${props.match.params.uploadId}`,
render: props => {
const { match, ...rest } = props
if (match && match.params.uploadId && match.params.calcId) {
......@@ -280,11 +284,13 @@ export default class App extends React.Component {
},
'uploads': {
exact: true,
singleton: true,
path: '/uploads',
render: props => <Uploads {...props} />
},
'uploadedEntry': {
path: '/uploads/:uploadId/:calcId',
key: (props) => `uploadedEntry/${props.match.params.uploadId}/${props.match.params.uploadId}`,
render: props => {
const { match, ...rest } = props
if (match && match.params.uploadId && match.params.calcId) {
......@@ -301,11 +307,16 @@ export default class App extends React.Component {
return (
<div>
{Object.keys(this.routes).map(route => (
<div key={route} style={{display: routeKey === route ? 'block' : 'none'}}>
{this.routes[route].render(props)}
</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>
)
}
......@@ -317,18 +328,20 @@ export default class App extends React.Component {
<BrowserRouter basename={process.env.PUBLIC_URL}>
<HelpProvider>
<ApiProvider>
<Navigation>
<Switch>
{Object.keys(this.routes).map(route => (
// eslint-disable-next-line react/jsx-key
<Route key={'nop'}
// eslint-disable-next-line react/no-children-prop
children={props => this.renderChildren(route, props)}
exact={this.routes[route].exact}
path={this.routes[route].path} />
))}
</Switch>
</Navigation>
<DomainProvider>
<Navigation>
<Switch>
{Object.keys(this.routes).map(route => (
// eslint-disable-next-line react/jsx-key
<Route key={'nop'}
// eslint-disable-next-line react/no-children-prop
children={props => this.renderChildren(route, props)}
exact={this.routes[route].exact}
path={this.routes[route].path} />
))}
</Switch>
</Navigation>
</DomainProvider>
</ApiProvider>
</HelpProvider>
</BrowserRouter>
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Typography } from '@material-ui/core'
class Quantity extends React.Component {
static propTypes = {
classes: PropTypes.object,
children: PropTypes.node,
label: PropTypes.string,
typography: PropTypes.string,
loading: PropTypes.bool,
placeholder: PropTypes.string,
noWrap: PropTypes.bool,
row: PropTypes.bool,
column: PropTypes.bool,
data: PropTypes.object,
quantity: PropTypes.string
}
static styles = theme => ({
root: {},
row: {
display: 'flex',
flexDirection: 'row',
'& > :not(:first-child)': {
marginLeft: theme.spacing.unit * 3
}
},
column: {
display: 'flex',
flexDirection: 'column',
'& > :not(:first-child)': {
marginTop: theme.spacing.unit * 1
}
}
})
render() {
const {classes, children, label, typography, loading, placeholder, noWrap, row, column, quantity, data} = this.props
let content = null
if (!loading) {
if (!(data && quantity && !data[quantity])) {
if (!children || children.length === 0) {
const value = data && quantity ? data[quantity] : null
if (value) {
content = <Typography noWrap={noWrap} variant={typography}>
{value}
</Typography>
} else {
content = <Typography noWrap={noWrap} variant={typography}>
<i>{placeholder || 'unavailable'}</i>
</Typography>
}
} else {
content = children
}
} else {
content = <Typography noWrap={noWrap} variant={typography}>
<i>{placeholder || 'unavailable'}</i>
</Typography>
}
}
if (row || column) {
return <div className={row ? classes.row : classes.column}>{children}</div>
} else {
return (
<div className={classes.root}>
<Typography noWrap variant="caption">{label || quantity}</Typography>
{loading ? <Typography noWrap={noWrap} variant={typography}>
<i>loading ...</i>
</Typography> : content}
</div>
)
}
}
}
export default withStyles(Quantity.styles)(Quantity)
......@@ -306,15 +306,15 @@ class Api {
async getInfo() {
if (!this._cachedInfo) {
this.onStartLoading()
const loadInfo = async() => {
const client = await this.swaggerPromise
return client.apis.info.get_info()
.catch(this.handleApiError)
.then(response => response.body)
}
this._cachedInfo = await loadInfo()
this.onFinishLoading()
this._cachedInfo = this.swaggerPromise
.then(client => {
return client.apis.info.get_info()
.catch(this.handleApiError)
.then(response => {
this.onFinishLoading()
return response.body
})
})
}
return this._cachedInfo
}
......@@ -515,7 +515,7 @@ class WithApiComponent extends React.Component {
if (notAuthorized) {
if (user) {
return (
<div style={{padding: 16}}>
<div>
<Typography variant="h6">Not Authorized</Typography>
<Typography>
You are not authorized to access this information. If someone send
......@@ -534,7 +534,7 @@ class WithApiComponent extends React.Component {
)
}
} else if (notFound) {
return <div style={{padding: 16}}>
return <div>
<Typography variant="h6">Not Found</Typography>
<Typography>
The information that you are trying to access does not exists.
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Card, CardHeader, CardContent } from '@material-ui/core'
import RawFiles from '../entry/RawFiles'
class DFTEntryCards extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
loading: PropTypes.bool
}
static styles = theme => ({
root: {}
})
render() {
const { classes, data } = this.props
return (
<Card className={classes.root}>
<CardHeader title="Raw files" />
<CardContent classes={{root: classes.cardContent}}>
<RawFiles data={data} />
</CardContent>
</Card>
)
}
}
export default withStyles(DFTEntryCards.styles)(DFTEntryCards)
import React from 'react'
import PropTypes from 'prop-types'
import { Typography } from '@material-ui/core'
import Quantity from '../Quantity'
export default class DFTEntryOverview extends React.Component {
static propTypes = {
data: PropTypes.object.isRequired,
loading: PropTypes.bool
}
render() {
const { data } = this.props
return (
<Quantity column>
<Quantity row>
<Quantity quantity="code_name" label='dft code' noWrap {...this.props} />
<Quantity quantity="code_version" label='dft code version' noWrap {...this.props} />
</Quantity>
<Quantity row>
<Quantity quantity="basis_set" label='basis set' noWrap {...this.props} />
<Quantity quantity="xc_functional" label='xc functional' noWrap {...this.props} />
</Quantity>
<Quantity row>
<Quantity quantity="system_type" label='system type' noWrap {...this.props} />
<Quantity quantity="crystal_system" label='crystal system' noWrap {...this.props} />
<Quantity quantity='spacegroup_symbol' label="spacegroup" noWrap {...this.props}>
<Typography noWrap>
{data.spacegroup_symbol} ({data.spacegroup})
</Typography>
</Quantity>
</Quantity>
</Quantity>
)
}
}
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Grid, Card, CardContent } from '@material-ui/core'
import PeriodicTable from '../search/PeriodicTable'
import QuantityHistogram from '../search/QuantityHistogram'
class DFTSearchAggregations extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
aggregations: PropTypes.object.isRequired,
metric: PropTypes.string.isRequired,
searchValues: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired
}
static styles = theme => ({
root: {},
quantity: {
marginTop: theme.spacing.unit * 2
},
quantityGrid: {
marginBottom: theme.spacing.unit * 2
}
})
handleAtomsChanged(atoms) {
const searchValues = {...this.props.searchValues}
searchValues.atoms = atoms
if (searchValues.atoms.length === 0) {
delete searchValues.atoms
}
this.props.onChange({searchValues: searchValues})
}
handleQuantityChanged(quantity, selection) {
const searchValues = {...this.props.searchValues}
if (selection) {
searchValues[quantity] = selection
} else {
delete searchValues[quantity]
}
this.props.onChange({searchValues: searchValues})
}
render() {
const { classes, aggregations, metric, searchValues } = this.props
const quantity = (key, title) => (<QuantityHistogram
classes={{root: classes.quantity}} title={title || key} width={300}
data={aggregations[key]} metric={metric}
value={searchValues[key]}
onChanged={(selection) => this.handleQuantityChanged(key, selection)}/>)
return (
<div className={classes.root}>
<Card>
<CardContent>
<PeriodicTable
aggregations={aggregations.atoms} metric={metric}
values={searchValues.atoms || []}
onChanged={(selection) => this.handleAtomsChanged(selection)}
/>
</CardContent>
</Card>
<Grid container spacing={24} className={classes.quantityGrid}>
<Grid item xs={4}>
{quantity('code_name', 'Code')}
</Grid>
<Grid item xs={4}>
{quantity('system', 'System type')}
{quantity('crystal_system', 'Crystal system')}
</Grid>
<Grid item xs={4}>
{quantity('basis_set', 'Basis set')}
{quantity('xc_functional', 'XC functionals')}
</Grid>
</Grid>
</div>
)
}
}
export default withStyles(DFTSearchAggregations.styles)(DFTSearchAggregations)
import React from 'react'
import PropTypes from 'prop-types'
import DFTSearchAggregations from './dft/DFTSearchAggregations'
import DFTEntryOverview from './dft/DFTEntryOverview'
import DFTEntryCards from './dft/DFTEntryCards'
import EMSSearchAggregations from './ems/EMSSearchAggregations'
import EMSEntryOverview from './ems/EMSEntryOverview'
import EMSEntryCards from './ems/EMSEntryCards'
import { withApi } from './api'
const DomainContext = React.createContext()
class DomainProviderBase extends React.Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
api: PropTypes.object.isRequired
}
domains = {
DFT: {
name: 'DFT',
/**
* A component that is used to render the search aggregations. The components needs
* to work with props: aggregations (the aggregation data from the api),
* searchValues (currently selected search values), metric (the metric key to use),
* onChange (callback to propagate searchValue changes).
*/
SearchAggregations: DFTSearchAggregations,
/**
* Metrics are used to show values for aggregations. Each metric has a key (used
* for API calls), a label (used in the select form), and result string (to show
* the overall amount in search results).
*/
searchMetrics: {
code_runs: {
label: 'Entries',
renderResultString: count => (<span><b>{count}</b> entries</span>)
},
unique_code_runs: {
label: 'Unique entries',
renderResultString: count => (<span> and <b>{count}</b> unique entries</span>)
},
total_energies: {
label: 'Total energy calculations',
renderResultString: count => (<span> with <b>{count}</b> total energy calculations</span>)
},
geometries: {
label: 'Unique geometries',
renderResultString: count => (<span> that simulate <b>{count}</b> unique geometries</span>)
},
datasets: {
label: 'Datasets',
renderResultString: count => (<span> curated in <b>{count}</b> datasets</span>)
}
},
/**
* An dict where each object represents a column. Possible keys are label, render.
* Default render
*/
searchResultColumns: {
formula: {
label: 'Formula'
},
code_name: {
label: 'Code'
},
basis_set: {
label: 'Basis set'
},
xc_functional: {
label: 'XT treatment'
},
system: {
label: 'System'
},
crystal_system: {
label: 'Crystal system'
},
spacegroup_symbol: {
label: 'Spacegroup'
}
},
/**
* A component to render the domain specific quantities in the metadata card of
* the entry view. Needs to work with props: data (the entry data from the API),
* loading (a bool with api loading status).
*/
EntryOverview: DFTEntryOverview,
/**
* A component to render additional domain specific cards in the
* the entry view. Needs to work with props: data (the entry data from the API),
* loading (a bool with api loading status).
*/
EntryCards: DFTEntryCards
},
EMS: {
name: 'EMS',
/**
* A component that is used to render the search aggregations. The components needs
* to work with props: aggregations (the aggregation data from the api),
* searchValues (currently selected search values), metric (the metric key to use),
* onChange (callback to propagate searchValue changes).
*/
SearchAggregations: EMSSearchAggregations,
/**
* Metrics are used to show values for aggregations. Each metric has a key (used
* for API calls), a label (used in the select form), and result string (to show
* the overall amount in search results).
*/
searchMetrics: {
code_runs: {
label: 'Entries',
renderResultString: count => (<span><b>{count}</b> entries</span>)
},
datasets: {
label: 'Datasets',
renderResultString: count => (<span> curated in <b>{count}</b> datasets</span>)
}
},
/**
* An dict where each object represents a column. Possible keys are label, render.
* Default render
*/
searchResultColumns: {
formula: {
label: 'Formula'
},
method: {
label: 'Method'
},
experiment_location: {
label: 'Location'
},
experiment_time: {
label: 'Date/Time',
render: time => new Date(time * 1000).toLocaleString()
}
},
/**
* A component to render the domain specific quantities in the metadata card of
* the entry view. Needs to work with props: data (the entry data from the API),
* loading (a bool with api loading status).
*/
EntryOverview: EMSEntryOverview,
/**
* A component to render additional domain specific cards in the
* the entry view. Needs to work with props: data (the entry data from the API),
* loading (a bool with api loading status).
*/
EntryCards: EMSEntryCards
}
}
state = {
domain: this.domains.DFT
}
componentDidMount() {
this.props.api.getInfo().then(info => {
this.setState({domain: this.domains[info.domain.name] || this.domains.DFT})
})
}
render() {
return (
<DomainContext.Provider value={this.state}>
{this.props.children}
</DomainContext.Provider>
)
}
}
export const DomainProvider = withApi(false, false)(DomainProviderBase)
export function withDomain(Component) {
function DomainConsumer(props) {
return (
<DomainContext.Consumer>
{state => <Component {...state} {...props}/>}
</DomainContext.Consumer>
)
}
return DomainConsumer
}
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Card, CardHeader, CardContent } from '@material-ui/core'
import RawFiles from '../entry/RawFiles'
class EMSEntryCards extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
loading: PropTypes.bool
}
static styles = theme => ({
root: {}
})
render() {
const { classes, data } = this.props
return (
<Card className={classes.root}>
<CardHeader title="Raw files" />
<CardContent classes={{root: classes.cardContent}}>
<RawFiles data={data} />
</CardContent>
</Card>
)
}
}
export default withStyles(EMSEntryCards.styles)(EMSEntryCards)
import React from 'react'
import PropTypes from 'prop-types'
import Quantity from '../Quantity'
import { Typography } from '@material-ui/core'
export default class EMSEntryOverview extends React.Component {
static propTypes = {
data: PropTypes.object.isRequired,
loading: PropTypes.bool
}
render() {
const { data } = this.props
return (
<Quantity row>
<Quantity column>
<Quantity row>
<Quantity quantity="formula" label="sample formula" noWrap {...this.props} />
<Quantity quantity="chemical" label="sample chemical" noWrap {...this.props} />
</Quantity>
<Quantity quantity="method" label="experimental method" noWrap {...this.props} />
<Quantity quantity="experiment_location" label="experiment location" noWrap {...this.props} />
<Quantity label="experiment time" {...this.props}>
<Typography noWrap>
{new Date(data.experiment_time * 1000).toLocaleString()}
</Typography>
</Quantity>
<Quantity label="data" {...this.props}>
<Typography noWrap>
<a href={data.repository_url}>{data.repository_name}</a>
</Typography>
</Quantity>
</Quantity>
<Quantity label="preview" {...this.props}>
<img alt="preview" style={{maxWidth: '100%', height: 'auto'}} src={data.preview_url}></img>
</Quantity>
</Quantity>
)
}
}
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Grid, Card, CardContent } from '@material-ui/core'
import PeriodicTable from '../search/PeriodicTable'
import QuantityHistogram from '../search/QuantityHistogram'
class EMSSearchAggregations extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
aggregations: PropTypes.object.isRequired,
metric: PropTypes.string.isRequired,
searchValues: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired
}
static styles = theme => ({
root: {},
quantity: {
marginTop: theme.spacing.unit * 2
},
quantityGrid: {
marginBottom: theme.spacing.unit * 2
}
})
handleAtomsChanged(atoms) {
const searchValues = {...this.props.searchValues}
searchValues.atoms = atoms
if (searchValues.atoms.length === 0) {
delete searchValues.atoms
}
this.props.onChange({searchValues: searchValues})
}
handleQuantityChanged(quantity, selection) {
const searchValues = {...this.props.searchValues}
if (selection) {
searchValues[quantity] = selection
} else {
delete searchValues[quantity]
}
this.props.onChange({searchValues: searchValues})
}
render() {
const { classes, aggregations, metric, searchValues } = this.props
const quantity = (key, title) => (<QuantityHistogram
classes={{root: classes.quantity}} title={title || key} width={300}
data={aggregations[key]} metric={metric}
value={searchValues[key]}
onChanged={(selection) => this.handleQuantityChanged(key, selection)}/>)
return (
<div className={classes.root}>
<Card>
<CardContent>
<PeriodicTable
aggregations={aggregations.atoms} metric={metric}
values={searchValues.atoms || []}
onChanged={(selection) => this.handleAtomsChanged(selection)}
/>
</CardContent>
</Card>
<Grid container spacing={24} className={classes.quantityGrid}>
<Grid item xs={6}>
{quantity('method', 'Method')}
</Grid>
<Grid item xs={6}>
{quantity('experiment_location', 'Location')}
</Grid>
</Grid>
</div>
)
}
}
export default withStyles(EMSSearchAggregations.styles)(EMSSearchAggregations)
......@@ -8,7 +8,7 @@ import { withApi } from '../api'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import Download from './Download'
class ArchiveCalcView extends React.Component {
class ArchiveEntryView extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
......@@ -130,4 +130,4 @@ class ArchiveCalcView extends React.Component {
}
}
export default compose(withApi(false, true), withStyles(ArchiveCalcView.styles))(ArchiveCalcView)
export default compose(withApi(false, true), withStyles(ArchiveEntryView.styles))(ArchiveEntryView)
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Tab, Tabs } from '@material-ui/core'
import ArchiveCalcView from './ArchiveCalcView'
import ArchiveEntryView from './ArchiveEntryView'
import ArchiveLogView from './ArchiveLogView'
import RepoCalcView from './RepoCalcView'
import RepoEntryView from './RepoEntryView'
class Calc extends React.Component {
static styles = theme => ({
root: {
},
content: {
padding: 0,
padding: `0 ${theme.spacing.unit * 3}px`,
maxWidth: 1024,
margin: 'auto'
}
......@@ -47,10 +47,10 @@ class Calc extends React.Component {
<div className={classes.content}>
<div style={viewIndex !== 0 ? {display: 'none'} : {}} >
<RepoCalcView {...calcProps} />
<RepoEntryView {...calcProps} />
</div>
<div style={viewIndex !== 1 ? {display: 'none'} : {}} >
<ArchiveCalcView {...calcProps} />
<ArchiveEntryView {...calcProps} />
</div>
<div style={viewIndex !== 2 ? {display: 'none'} : {}} >
<ArchiveLogView {...calcProps} />
......
......@@ -9,11 +9,10 @@ import Download from './Download'
class RawFiles extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired,
files: PropTypes.arrayOf(PropTypes.string).isRequired,
data: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
user: PropTypes.object
user: PropTypes.object,
loading: PropTypes.number.isRequired
}
static styles = theme => ({
......@@ -43,10 +42,11 @@ class RawFiles extends React.Component {
}
render() {
const {classes, files, uploadId, calcId} = this.props
const {classes, data: {files, upload_id, calc_id}, loading} = this.props
const availableFiles = files || []
const {selectedFiles} = this.state
const someSelected = selectedFiles.length > 0
const allSelected = files.length === selectedFiles.length && someSelected
const allSelected = availableFiles.length === selectedFiles.length && someSelected
return (
<div className={classes.root}>
......@@ -56,27 +56,28 @@ class RawFiles extends React.Component {
control={
<Checkbox value="select_all" checked={allSelected}
indeterminate={!allSelected && someSelected}
onChange={() => this.setState({selectedFiles: allSelected ? [] : files.slice()})}
onChange={() => this.setState({selectedFiles: allSelected ? [] : availableFiles.slice()})}
/>
}
/>
<FormLabel className={classes.formLabel}>
{selectedFiles.length}/{files.length} files selected
{selectedFiles.length}/{availableFiles.length} files selected
</FormLabel>
<Download component={IconButton} disabled={selectedFiles.length === 0}
tooltip="download selected files"
url={(selectedFiles.length === 1) ? `raw/${uploadId}/${selectedFiles[0]}` : `raw/${uploadId}?files=${encodeURIComponent(selectedFiles.join(','))}`}
fileName={selectedFiles.length === 1 ? this.label(selectedFiles[0]) : `${calcId}.zip`}
url={(selectedFiles.length === 1) ? `raw/${upload_id}/${selectedFiles[0]}` : `raw/${calc_id}?files=${encodeURIComponent(selectedFiles.join(','))}`}
fileName={selectedFiles.length === 1 ? this.label(selectedFiles[0]) : `${calc_id}.zip`}
>
<DownloadIcon />
</Download>
</FormGroup>
<Divider />
<FormGroup row>
{files.map((file, index) => (
{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}
/>
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Typography, Select, MenuItem, Grid } from '@material-ui/core'
import { withStyles, Select, MenuItem, Card, CardContent, CardHeader } from '@material-ui/core'
import * as d3 from 'd3'
import { scaleBand, scalePow } from 'd3-scale'
import chroma from 'chroma-js'
......@@ -22,11 +22,9 @@ class QuantityHistogram extends React.Component {
}
static styles = theme => ({
root: {
},
title: {
fontWeight: 'bold'
root: {},
content: {
paddingTop: 0
}
})
......@@ -183,12 +181,11 @@ class QuantityHistogram extends React.Component {
const { classes, title } = this.props
return (
<div className={classes.root} ref={this.container}>
<Grid container justify="space-between">
<Grid item>
<Typography variant="body1" className={classes.title}>{title}</Typography>
</Grid>
<Grid item>
<Card classes={{root: classes.root}}>
<CardHeader
title={title}
titleTypographyProps={{variant: 'body1'}}
action={(
<Select
value={this.state.scalePower}
onChange={(event) => this.setState({scalePower: event.target.value})}
......@@ -200,10 +197,14 @@ class QuantityHistogram extends React.Component {
<MenuItem value={0.25}>1/4</MenuItem>
<MenuItem value={0.125}>1/8</MenuItem>
</Select>
</Grid>
</Grid>
<svg ref={this.svgEl} />
</div>
)}
/>
<CardContent classes={{root: classes.content}}>
<div ref={this.container}>
<svg ref={this.svgEl} />
</div>
</CardContent>
</Card>
)
}
}
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@material-ui/core'
import { withDomain } from '../domains'
import { compose } from 'recompose'
class SearchAggregationsUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
data: PropTypes.object.isRequired,
total_metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
aggregation_metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
searchValues: PropTypes.object.isRequired,
domain: PropTypes.object.isRequired,
showDetails: PropTypes.bool
}
static styles = theme => ({
root: {
marginTop: theme.spacing.unit
},
details: {
marginTop: theme.spacing.unit * 3
}
})
handleMetricChange(metric) {
const metrics = metric === 'code_runs' ? [] : [metric]
this.setState({metric: metric})
this.props.onChange({total_metrics: metrics, aggregation_metrics: metrics})
}
handleSearchChanged(searchValues) {
this.props.onChange({searchValues: searchValues})
}
render() {
const { classes, data, total_metrics, searchValues, domain, onChange, showDetails } = this.props
const { aggregations, metrics } = data
const selectedMetric = total_metrics.length === 0 ? 'code_runs' : total_metrics[0]
const useMetric = Object.keys(metrics).find(metric => metric !== 'code_runs') || 'code_runs'
const metricsDefinitions = domain.searchMetrics
return (
<div className={classes.root}>
<div className={classes.details} style={showDetails ? {} : {display: 'none'}}>
<FormControl>
<FormLabel>Metric used in statistics: </FormLabel>
<FormGroup row>
{Object.keys(metricsDefinitions).map(metric => (
<FormControlLabel key={metric}
control={
<Checkbox checked={selectedMetric === metric} onChange={() => this.handleMetricChange(metric)} value={metric} />
}
label={metricsDefinitions[metric].label}
/>
))}
</FormGroup>
</FormControl>
<domain.SearchAggregations aggregations={aggregations} searchValues={searchValues} metric={useMetric} onChange={onChange} />
</div>
</div>
)
}
}
const SearchAggregations = compose(withDomain, withStyles(SearchAggregationsUnstyled.styles))(SearchAggregationsUnstyled)
Object.assign(SearchAggregations, {
defaultState: {
aggregation_metrics: [],
total_metrics: [],
searchValues: {}
}
})
export default SearchAggregations
......@@ -7,6 +7,8 @@ import match from 'autosuggest-highlight/match'
import parse from 'autosuggest-highlight/parse'
import Paper from '@material-ui/core/Paper'
import MenuItem from '@material-ui/core/MenuItem'
import { Chip } from '@material-ui/core'
import { repoPrimaryColor } from '../../config'
function renderInput(inputProps) {
const { classes, autoFocus, value, onChange, onAdd, onDelete, chips, ref, ...other } = inputProps
......@@ -19,6 +21,22 @@ function renderInput(inputProps) {
onDelete={onDelete}
value={chips}
inputRef={ref}
chipRenderer={
({ value, text, isFocused, isDisabled, handleClick, handleDelete, className }, key) => (
<Chip
key={key}
className={className}
style={{
pointerEvents: isDisabled ? 'none' : undefined,
backgroundColor: isFocused ? repoPrimaryColor[500] : undefined,
color: isFocused ? 'white' : 'black'
}}
onClick={handleClick}
onDelete={handleDelete}
label={text}
/>
)
}
{...other}
/>
)
......@@ -74,14 +92,8 @@ class SearchBar extends React.Component {
}
static styles = theme => ({
root: {
width: '100%',
minWidth: 500,
maxWidth: 900,
margin: 'auto',
marginBottom: theme.spacing.unit * 3
},
container: {
root: {},
autosuggestRoot: {
position: 'relative'
},
suggestionsContainerOpen: {
......@@ -99,9 +111,6 @@ class SearchBar extends React.Component {
padding: 0,
listStyleType: 'none'
},
divider: {
height: theme.spacing.unit * 2
},
textField: {
width: '100%'
}
......@@ -213,35 +222,33 @@ class SearchBar extends React.Component {
const { classes, searchValues, onChanged, ...rest } = this.props
return (
<div className={classes.root} >
<Autosuggest
theme={{
container: classes.container,
suggestionsContainerOpen: classes.suggestionsContainerOpen,
suggestionsList: classes.suggestionsList,
suggestion: classes.suggestion
}}
renderInputComponent={renderInput}
suggestions={this.state.suggestions}
onSuggestionsFetchRequested={this.handleSuggestionsFetchRequested}
onSuggestionsClearRequested={this.handleSuggestionsClearRequested}
renderSuggestionsContainer={renderSuggestionsContainer}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
onSuggestionSelected={(e, { suggestionValue }) => { this.handleAddChip(suggestionValue); e.preventDefault() }}
focusInputOnSuggestionClick={false}
inputProps={{
classes,
chips: this.getChips(),
onChange: this.handleTextFieldInputChange,
value: this.state.textFieldInput,
onAdd: (chip) => this.handleAddChip(chip),
onBeforeAdd: (chip) => this.handleBeforeAddChip(chip),
onDelete: (chip, index) => this.handleDeleteChip(chip, index),
...rest
}}
/>
</div>
<Autosuggest
theme={{
container: classes.autosuggestRoot,
suggestionsContainerOpen: classes.suggestionsContainerOpen,
suggestionsList: classes.suggestionsList,
suggestion: classes.suggestion
}}
renderInputComponent={renderInput}
suggestions={this.state.suggestions}
onSuggestionsFetchRequested={this.handleSuggestionsFetchRequested}
onSuggestionsClearRequested={this.handleSuggestionsClearRequested}
renderSuggestionsContainer={renderSuggestionsContainer}
getSuggestionValue={getSuggestionValue}
renderSuggestion={renderSuggestion}
onSuggestionSelected={(e, { suggestionValue }) => { this.handleAddChip(suggestionValue); e.preventDefault() }}
focusInputOnSuggestionClick={false}
inputProps={{
classes,
chips: this.getChips(),
onChange: this.handleTextFieldInputChange,
value: this.state.textFieldInput,
onAdd: (chip) => this.handleAddChip(chip),
onBeforeAdd: (chip) => this.handleBeforeAddChip(chip),
onDelete: (chip, index) => this.handleDeleteChip(chip, index),
...rest
}}
/>
)
}
}
......
......@@ -2,35 +2,59 @@ import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import { FormControl, FormControlLabel, Checkbox, FormGroup,
FormLabel, IconButton, MuiThemeProvider } from '@material-ui/core'
FormLabel, IconButton, Typography, Divider, Tooltip } from '@material-ui/core'
import { compose } from 'recompose'
import { withErrors } from '../errors'
import AnalyticsIcon from '@material-ui/icons/Settings'
import { analyticsTheme } from '../../config'
import Link from 'react-router-dom/Link'
import { withApi, DisableOnLoading } from '../api'
import SearchBar from './SearchBar'
import SearchResultList from './SearchResultList'
import SearchStatistics from './SearchStatistics'
import SearchAggregations from './SearchAggregations'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import ExpandLessIcon from '@material-ui/icons/ExpandLess'
import { withDomain } from '../domains'
class SearchPage extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
user: PropTypes.object,
raiseError: PropTypes.func.isRequired
raiseError: PropTypes.func.isRequired,
domain: PropTypes.object,
loading: PropTypes.number
}
static styles = theme => ({
root: {
padding: theme.spacing.unit * 3
},
selectFormGroup: {
paddingLeft: theme.spacing.unit * 3
searchEntry: {
minWidth: 500,
maxWidth: 900,
margin: 'auto',
width: '100%'
},
selectLabel: {
padding: theme.spacing.unit * 2
}
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: {}
})
state = {
......@@ -44,11 +68,12 @@ class SearchPage extends React.Component {
},
owner: 'all',
searchState: {
...SearchStatistics.defaultState
...SearchAggregations.defaultState
},
searchResultListState: {
...SearchResultList.defaultState
}
},
showDetails: true
}
constructor(props) {
......@@ -56,6 +81,7 @@ class SearchPage extends React.Component {
this.updateSearchResultList = this.updateSearchResultList.bind(this)
this.updateSearch = this.updateSearch.bind(this)
this.handleClickExpand = this.handleClickExpand.bind(this)
}
updateSearchResultList(changes) {
......@@ -107,11 +133,15 @@ class SearchPage extends React.Component {
this.update({owner: owner})
}
handleClickExpand() {
this.setState({showDetails: !this.state.showDetails})
}
render() {
const { classes, user } = this.props
const { data, searchState, searchResultListState } = this.state
const { classes, user, domain, loading } = this.props
const { data, searchState, searchResultListState, showDetails } = this.state
const { searchValues } = searchState
const { pagination: { total } } = data
const { pagination: { total }, metrics } = data
const ownerLabel = {
all: 'All entries',
......@@ -120,56 +150,77 @@ class SearchPage extends React.Component {
staging: 'Only entries from your staging area'
}
const useMetric = Object.keys(metrics).find(metric => metric !== 'code_runs') || 'code_runs'
const helperText = <span>
There are {Object.keys(domain.searchMetrics).map(key => {
return (key === useMetric || key === 'code_runs') ? <span key={key}>
{domain.searchMetrics[key].renderResultString(!loading && metrics[key] !== undefined ? metrics[key] : '...')}
</span> : ''
})}{Object.keys(searchValues).length ? ' left' : ''}.
</span>
return (
<div className={classes.root}>
<DisableOnLoading>
{ user
? <FormControl>
<FormLabel>Filter entries and show: </FormLabel>
<FormGroup row>
{['all', 'public', 'user', 'staging'].map(owner => (
<FormControlLabel key={owner}
control={
<Checkbox checked={this.state.owner === owner} onChange={() => this.handleOwnerChange(owner)} value="owner" />
}
label={ownerLabel[owner]}
/>
))}
</FormGroup>
</FormControl> : ''
? <div className={classes.searchEntry}>
<FormControl>
<FormLabel>Filter entries and show: </FormLabel>
<FormGroup row>
{['all', 'public', 'user', 'staging'].map(owner => (
<FormControlLabel key={owner}
control={
<Checkbox checked={this.state.owner === owner} onChange={() => this.handleOwnerChange(owner)} value="owner" />
}
label={ownerLabel[owner]}
/>
))}
</FormGroup>
</FormControl>
</div> : ''
}
<SearchBar
fullWidth fullWidthInput={false} label="search" placeholder="enter atoms or other quantities"
data={data} searchValues={searchValues}
onChanged={values => this.updateSearch({searchValues: values})}
/>
<SearchStatistics data={data} {...searchState} onChange={this.updateSearch} />
<FormGroup className={classes.selectFormGroup} row>
<FormLabel classes={{root: classes.selectLabel}} style={{flexGrow: 1}}>
</FormLabel>
<FormLabel classes={{root: classes.selectLabel}}>
Analyse {total} code runs in an analytics notebook
</FormLabel>
<MuiThemeProvider theme={analyticsTheme}>
<IconButton color="primary" component={Link} to={`/analytics`}>
<AnalyticsIcon />
<div className={classes.search}>
<SearchBar classes={{autosuggestRoot: classes.searchBar}}
fullWidth fullWidthInput={false} helperText={helperText}
label="search"
placeholder="enter atoms, codes, functionals, or other quantity values"
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>
</MuiThemeProvider>
</FormGroup>
<SearchResultList
data={data} total={total}
onChange={this.updateSearchResultList}
{...searchResultListState}
/>
</Tooltip>
</div>
<div className={classes.searchEntry}>
<SearchAggregations
data={data} {...searchState} onChange={this.updateSearch}
showDetails={showDetails}
/>
</div>