Commit 4e3766d8 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Added domain select to search.

parent 2c01e46d
......@@ -5,15 +5,14 @@ import Markdown from './Markdown'
import { appBase, optimadeBase, apiBase, debug, consent } from '../config'
import { compose } from 'recompose'
import { withApi } from './api'
import { withDomain } from './domains'
import packageJson from '../../package.json'
import { domains } from './domains'
class About extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
info: PropTypes.object,
domains: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired
}
......@@ -24,7 +23,7 @@ class About extends React.Component {
})
render() {
const { classes, domains, info } = this.props
const { classes, info } = this.props
return (
<div className={classes.root}>
......@@ -126,4 +125,4 @@ class About extends React.Component {
}
}
export default compose(withApi(), withDomain, withStyles(About.styles))(About)
export default compose(withApi(), withStyles(About.styles))(About)
......@@ -23,7 +23,6 @@ import { help as entryHelp, default as EntryPage } from './entry/EntryPage'
import About from './About'
import LoginLogout from './LoginLogout'
import { guiBase, consent, nomadTheme } from '../config'
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'
......@@ -31,7 +30,6 @@ import Markdown from './Markdown'
import {help as uploadHelp, default as UploadPage} from './uploads/UploadPage'
import ResolvePID from './entry/ResolvePID'
import DatasetPage from './DatasetPage'
import { capitalize } from '../utils'
import { amber } from '@material-ui/core/colors'
import KeepState from './KeepState'
import {help as userdataHelp, default as UserdataPage} from './UserdataPage'
......@@ -68,8 +66,7 @@ class NavigationUnstyled extends React.Component {
children: PropTypes.any,
location: PropTypes.object.isRequired,
loading: PropTypes.number.isRequired,
raiseError: PropTypes.func.isRequired,
domains: PropTypes.object.isRequired
raiseError: PropTypes.func.isRequired
}
static styles = theme => ({
......@@ -164,7 +161,7 @@ class NavigationUnstyled extends React.Component {
'/uploads': 'Upload and Publish Data',
'/userdata': 'Manage Your Data',
'/metainfo': 'The NOMAD Meta Info',
'/entry': capitalize(this.props.domains.entryLabel),
'/entry': 'Entry',
'/dataset': 'Dataset'
}
......@@ -309,7 +306,7 @@ class NavigationUnstyled extends React.Component {
}
}
const Navigation = compose(withRouter, withErrors, withApi(false), withDomain, withStyles(NavigationUnstyled.styles))(NavigationUnstyled)
const Navigation = compose(withRouter, withErrors, withApi(false), withStyles(NavigationUnstyled.styles))(NavigationUnstyled)
class LicenseAgreementUnstyled extends React.Component {
static propTypes = {
......@@ -487,20 +484,18 @@ export default class App extends React.Component {
<MuiThemeProvider theme={nomadTheme}>
<ErrorSnacks>
<ApiProvider>
<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>
<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>
</ApiProvider>
</ErrorSnacks>
<LicenseAgreement />
......
......@@ -16,7 +16,6 @@ import Tooltip from '@material-ui/core/Tooltip'
import ViewColumnIcon from '@material-ui/icons/ViewColumn'
import { Popover, List, ListItemText, ListItem, Collapse } from '@material-ui/core'
import { compose } from 'recompose'
import { withDomain } from './domains'
import _ from 'lodash'
class DataTableToolbarUnStyled extends React.Component {
......@@ -297,6 +296,12 @@ class DataTableUnStyled extends React.Component {
selectedColumns: null
}
componentDidUpdate(prevProps) {
if (prevProps.columns !== this.props.columns) {
this.setState({selectedColumns: this.props.selectedColumns})
}
}
handleRequestSort(event, property) {
const { orderBy, order, onOrderChanged } = this.props
const isDesc = orderBy === property && order === 'desc'
......@@ -514,4 +519,4 @@ class DataTableUnStyled extends React.Component {
}
}
export default compose(withDomain, withStyles(DataTableUnStyled.styles))(DataTableUnStyled)
export default compose(withStyles(DataTableUnStyled.styles))(DataTableUnStyled)
......@@ -3,8 +3,6 @@ import { withStyles, Button, IconButton, Dialog, DialogTitle, DialogContent, Dia
import Markdown from './Markdown'
import PropTypes from 'prop-types'
import HelpIcon from '@material-ui/icons/Help'
import { compose } from 'recompose'
import { withDomain } from './domains'
export const HelpContext = React.createContext()
......@@ -12,10 +10,9 @@ class HelpDialogUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
title: PropTypes.string,
content: PropTypes.func.isRequired,
content: PropTypes.string.isRequired,
icon: PropTypes.node,
maxWidth: PropTypes.string,
domains: PropTypes.object.isRequired
maxWidth: PropTypes.string
}
static styles = theme => ({
......@@ -41,7 +38,7 @@ class HelpDialogUnstyled extends React.Component {
}
render() {
const {classes, title, content, icon, maxWidth, domains, ...rest} = this.props
const {classes, title, content, icon, maxWidth, ...rest} = this.props
return (
<div className={classes.root}>
<Tooltip title={title}>
......@@ -56,7 +53,7 @@ class HelpDialogUnstyled extends React.Component {
>
<DialogTitle>{title || 'Help'}</DialogTitle>
<DialogContent>
<Markdown>{content(domains)}</Markdown>
<Markdown>{content}</Markdown>
</DialogContent>
<DialogActions>
<Button onClick={() => this.handleClose()} color="primary">
......@@ -69,4 +66,4 @@ class HelpDialogUnstyled extends React.Component {
}
}
export default compose(withDomain, withStyles(HelpDialogUnstyled.styles))(HelpDialogUnstyled)
export default withStyles(HelpDialogUnstyled.styles)(HelpDialogUnstyled)
......@@ -2,8 +2,9 @@ import React from 'react'
import { withApi } from './api'
import Search from './search/Search'
import SearchContext from './search/SearchContext'
import { domains } from './domains'
export const help = domain => `
export const help = `
This page allows you to **inspect** and **manage** you own data. It is similar to the
*search page*, but it will only show data that was uploaded by you or is shared with you.
......@@ -58,6 +59,7 @@ class UserdataPage extends React.Component {
return (
<div>
<SearchContext
defaultDomain={domains.dft}
{...this.props}
ownerTypes={['user', 'staging']} initialQuery={{owner: 'user'}}
initialRequest={{order_by: 'upload_time', uploads: true}}
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Grid } from '@material-ui/core'
import QuantityHistogram from '../search/QuantityHistogram'
import { Grid } from '@material-ui/core'
import { Quantity } from '../search/QuantityHistogram'
import SearchContext from '../search/SearchContext'
import { withApi } from '../api'
class QuantityUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
quantity: PropTypes.string.isRequired,
metric: PropTypes.string.isRequired,
title: PropTypes.string,
scale: PropTypes.number
}
static styles = theme => ({
root: {
marginTop: theme.spacing.unit * 2
}
})
static contextType = SearchContext.type
render() {
const {classes, scale, quantity, title, ...props} = this.props
const {state: {response, query}, setQuery} = this.context
return <QuantityHistogram
classes={{root: classes.root}}
width={300}
defaultScale={scale || 1}
title={title || quantity}
data={response.statistics[quantity]}
value={query[quantity]}
onChanged={selection => setQuery({...query, [quantity]: selection})}
{...props} />
}
}
const Quantity = withStyles(QuantityUnstyled.styles)(QuantityUnstyled)
class DFTSearchAggregations extends React.Component {
static propTypes = {
info: PropTypes.object
......
import React from 'react'
import PropTypes from 'prop-types'
import DFTSearchAggregations from './dft/DFTSearchAggregations'
import DFTEntryOverview from './dft/DFTEntryOverview'
import DFTEntryCards from './dft/DFTEntryCards'
......@@ -7,227 +6,198 @@ import EMSSearchAggregations from './ems/EMSSearchAggregations'
import EMSEntryOverview from './ems/EMSEntryOverview'
import EMSEntryCards from './ems/EMSEntryCards'
const DomainContext = React.createContext()
export class DomainProvider extends React.Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired
}
domains = {
export const domains = ({
dft: {
name: 'DFT',
key: 'dft',
about: 'This include data from many computational material science codes',
entryLabel: 'entry',
dft: {
name: 'DFT',
about: 'This include data from many computational material science codes',
entryLabel: 'entry',
entryLabelPlural: 'entries',
entryTitle: data => data.dft && data.dft.code_name ? data.dft.code_name + ' run' : 'Code run',
searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values',
/**
* 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',
tooltip: 'The statistics will show the number of database entry. Each set of input/output files that represents a code run is an entry.',
renderResultString: count => (<span><b>{count.toLocaleString()}</b> entr{count === 1 ? 'y' : 'ies'}</span>)
},
unique_entries: {
label: 'Unique entries',
tooltip: 'Counts duplicates only once.',
renderResultString: count => (<span> and <b>{count.toLocaleString()}</b> unique entr{count === 1 ? 'y' : 'ies'}</span>)
},
// total_energies: {
// label: 'Total energy calculations',
// tooltip: 'Aggregates the number of total energy calculations as each entry can contain many calculations.',
// renderResultString: count => (<span> with <b>{count.toLocaleString()}</b> total energy calculation{count === 1 ? '' : 's'}</span>)
// },
'dft.calculations': {
label: 'Single configuration calculations',
shortLabel: 'SCC',
tooltip: 'Aggregates the number of single configuration calculations (e.g. total energy calculations) as each entry can contain many calculations.',
renderResultString: count => (<span> with <b>{count.toLocaleString()}</b> single configuration calculation{count === 1 ? '' : 's'}</span>)
},
// The unique_geometries search aggregates unique geometries based on 10^8 hashes.
// This takes to long in elastic search for a reasonable user experience.
// Therefore, we only support geometries without uniqueness check
'dft.geometries': {
label: 'Geometries',
shortLabel: 'Geometries',
tooltip: 'Aggregates the number of simulated system geometries in all entries.',
renderResultString: count => (<span> that simulate <b>{count.toLocaleString()}</b> unique geometrie{count === 1 ? '' : 's'}</span>)
},
datasets: {
label: 'Datasets',
tooltip: 'Shows statistics in terms of datasets that entries belong to.',
renderResultString: count => (<span> curated in <b>{count.toLocaleString()}</b> dataset{count === 1 ? '' : 's'}</span>)
}
entryLabelPlural: 'entries',
entryTitle: data => data.dft && data.dft.code_name ? data.dft.code_name + ' run' : 'Code run',
searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values',
/**
* 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',
tooltip: 'The statistics will show the number of database entry. Each set of input/output files that represents a code run is an entry.',
renderResultString: count => (<span><b>{count.toLocaleString()}</b> entr{count === 1 ? 'y' : 'ies'}</span>)
},
defaultSearchMetric: 'code_runs',
additionalSearchKeys: {
raw_id: {},
external_id: {},
upload_id: {},
calc_id: {},
paths: {},
pid: {},
mainfile: {},
calc_hash: {},
formula: {},
'dft.optimade': {},
'dft.quantities': {},
'dft.spacegroup': {},
'dft.spacegroup_symbol': {},
'dft.labels': {},
upload_name: {}
unique_entries: {
label: 'Unique entries',
tooltip: 'Counts duplicates only once.',
renderResultString: count => (<span> and <b>{count.toLocaleString()}</b> unique entr{count === 1 ? 'y' : 'ies'}</span>)
},
/**
* An dict where each object represents a column. Possible keys are label, render.
* Default render
*/
searchResultColumns: {
'dft.formula': {
label: 'Formula',
supportsSort: true
},
'dft.code_name': {
label: 'Code',
supportsSort: true
},
'dft.basis_set': {
label: 'Basis set',
supportsSort: true
},
'dft.xc_functional': {
label: 'XT treatment',
supportsSort: true
},
'dft.system': {
label: 'System',
supportsSort: true
},
'dft.crystal_system': {
label: 'Crystal system',
supportsSort: true
},
'dft.spacegroup_symbol': {
label: 'Spacegroup',
supportsSort: true
},
'dft.spacegroup': {
label: 'Spacegroup (number)',
supportsSort: true
}
// total_energies: {
// label: 'Total energy calculations',
// tooltip: 'Aggregates the number of total energy calculations as each entry can contain many calculations.',
// renderResultString: count => (<span> with <b>{count.toLocaleString()}</b> total energy calculation{count === 1 ? '' : 's'}</span>)
// },
'dft.calculations': {
label: 'Single configuration calculations',
shortLabel: 'SCC',
tooltip: 'Aggregates the number of single configuration calculations (e.g. total energy calculations) as each entry can contain many calculations.',
renderResultString: count => (<span> with <b>{count.toLocaleString()}</b> single configuration calculation{count === 1 ? '' : 's'}</span>)
},
defaultSearchResultColumns: ['dft.formula', 'dft.code_name', 'dft.system', 'dft.crystal_system', 'dft.spacegroup_symbol'],
/**
* 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
// The unique_geometries search aggregates unique geometries based on 10^8 hashes.
// This takes to long in elastic search for a reasonable user experience.
// Therefore, we only support geometries without uniqueness check
'dft.geometries': {
label: 'Geometries',
shortLabel: 'Geometries',
tooltip: 'Aggregates the number of simulated system geometries in all entries.',
renderResultString: count => (<span> that simulate <b>{count.toLocaleString()}</b> unique geometrie{count === 1 ? '' : 's'}</span>)
},
datasets: {
label: 'Datasets',
tooltip: 'Shows statistics in terms of datasets that entries belong to.',
renderResultString: count => (<span> curated in <b>{count.toLocaleString()}</b> dataset{count === 1 ? '' : 's'}</span>)
}
},
defaultSearchMetric: 'code_runs',
additionalSearchKeys: {
raw_id: {},
external_id: {},
upload_id: {},
calc_id: {},
paths: {},
pid: {},
mainfile: {},
calc_hash: {},
formula: {},
'dft.optimade': {},
'dft.quantities': {},
'dft.spacegroup': {},
'dft.spacegroup_symbol': {},
'dft.labels': {},
upload_name: {}
},
ems: {
name: 'EMS',
about: 'This is metadata from material science experiments',
entryLabel: 'entry',
entryLabelPlural: 'entries',
entryTitle: () => 'Experiment',
searchPlaceholder: 'enter atoms, experimental methods, or other quantity values',
/**
* 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',
tooltip: 'Statistics will show the number of database entry. Usually each entry represents a single experiment.',
renderResultString: count => (<span><b>{count}</b> entries</span>)
},
datasets: {
label: 'Datasets',
tooltip: 'Shows statistics in terms of datasets that entries belong to.',
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: {
'dft.formula': {
label: 'Formula',
supportsSort: true
},
defaultSearchMetric: 'code_runs',
/**
* 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 => time !== 'unavailable' ? new Date(time * 1000).toLocaleString() : time
}
'dft.code_name': {
label: 'Code',
supportsSort: true
},
/**
* 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
}
}
render() {
return (
<DomainContext.Provider value={{domains: this.domains}}>
{this.props.children}
</DomainContext.Provider>
)
}
}
export function withDomain(Component) {
function DomainConsumer(props) {
return (
<DomainContext.Consumer>
{state => <Component {...state} {...props}/>}
</DomainContext.Consumer>
)
'dft.basis_set': {
label: 'Basis set',
supportsSort: true
},
'dft.xc_functional': {
label: 'XT treatment',
supportsSort: true
},
'dft.system': {
label: 'System',
supportsSort: true
},
'dft.crystal_system': {
label: 'Crystal system',
supportsSort: true
},
'dft.spacegroup_symbol': {
label: 'Spacegroup',
supportsSort: true
},
'dft.spacegroup': {
label: 'Spacegroup (number)',
supportsSort: true
}
},
defaultSearchResultColumns: ['dft.formula', 'dft.code_name', 'dft.system', 'dft.crystal_system', 'dft.spacegroup_symbol'],
/**
* 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',
key: 'ems',
about: 'This is metadata from material science experiments',
entryLabel: 'entry',
entryLabelPlural: 'entries',
entryTitle: () => 'Experiment',