Commit c0248a0a authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Refactored and enhanced the search page.

parent 93dab2e1
Pipeline #46704 passed with stages
in 16 minutes and 55 seconds
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Grid } from '@material-ui/core'
import { withStyles, Grid, Card, CardContent } from '@material-ui/core'
import PeriodicTable from '../search/PeriodicTable'
import QuantityHistogram from '../search/QuantityHistogram'
......@@ -15,14 +15,11 @@ class DFTSearchAggregations extends React.Component {
static styles = theme => ({
root: {},
quantityGrid: {
minWidth: 524,
maxWidth: 924,
margin: 'auto',
width: '100%'
},
quantity: {
marginTop: theme.spacing.unit * 2
},
quantityGrid: {
marginBottom: theme.spacing.unit * 2
}
})
......@@ -56,25 +53,28 @@ class DFTSearchAggregations extends React.Component {
return (
<div className={classes.root}>
<PeriodicTable
aggregations={aggregations.atoms} metric={metric}
values={searchValues.atoms || []}
onChanged={(selection) => this.handleAtomsChanged(selection)}
/>
<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('system', 'System')}
{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 item xs={4}>
{quantity('code_name', 'Code')}
</Grid>
</Grid>
</div>
)
......
......@@ -29,6 +29,14 @@ export class DomainProvider extends React.Component {
* 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>)
......@@ -36,6 +44,10 @@ export class DomainProvider extends React.Component {
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>)
}
},
/**
......
......@@ -3,7 +3,6 @@ import PropTypes from 'prop-types'
import { withStyles, Divider, Card, CardContent, Grid, CardHeader, Fab } from '@material-ui/core'
import { withApi } from '../api'
import { compose } from 'recompose'
import RawFiles from './RawFiles'
import Download from './Download'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import ApiDialogButton from '../ApiDialogButton'
......
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, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails,
FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox, Typography
} from '@material-ui/core'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import { withStyles, FormControl, FormLabel, FormGroup, FormControlLabel, Checkbox } from '@material-ui/core'
import { withDomain } from '../domains'
import { compose } from 'recompose'
......@@ -15,29 +12,16 @@ class SearchAggregationsUnstyled extends React.Component {
total_metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
aggregation_metrics: PropTypes.arrayOf(PropTypes.string).isRequired,
searchValues: PropTypes.object.isRequired,
domain: PropTypes.object.isRequired
domain: PropTypes.object.isRequired,
showDetails: PropTypes.bool
}
static styles = theme => ({
root: {},
searchDetails: {
padding: 0,
paddingBottom: theme.spacing.unit * 2,
display: 'block',
overflowX: 'auto'
root: {
marginTop: theme.spacing.unit
},
searchSummary: {
overflowX: 'auto'
},
summary: {
textAlign: 'center',
marginTop: theme.spacing.unit * 2
},
statistics: {
minWidth: 500,
maxWidth: 900,
margin: 'auto',
width: '100%'
details: {
marginTop: theme.spacing.unit * 3
}
})
......@@ -52,56 +36,31 @@ class SearchAggregationsUnstyled extends React.Component {
}
render() {
const { classes, data, total_metrics, searchValues, domain, onChange } = this.props
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 = {
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>)
},
...domain.searchMetrics,
datasets: {
label: 'Datasets',
renderResultString: count => (<span> curated in <b>{metrics.datasets}</b> datasets</span>)
}
}
const metricsDefinitions = domain.searchMetrics
return (
<ExpansionPanel className={classes.root}>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} className={classes.searchSummary}>
<Typography variant="h6" style={{textAlign: 'center', width: '100%', fontWeight: 'normal'}}>
Found {Object.keys(metricsDefinitions).map(key => {
return (key === useMetric || key === 'code_runs') ? <span key={key}>{metricsDefinitions[key].renderResultString(metrics[key])}</span> : ''
})}.
</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.searchDetails}>
<div className={classes.statistics}>
<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>
</div>
<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} />
</ExpansionPanelDetails>
</ExpansionPanel>
</div>
</div>
)
}
}
......
......@@ -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 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 = {
......@@ -48,7 +72,8 @@ class SearchPage extends React.Component {
},
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] ? 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})}
/>
<SearchAggregations 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'}>