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

Added search bar. Removed non implemented nav items.

parent 7c055563
Pipeline #45281 passed with stages
in 33 minutes and 43 seconds
......@@ -6,6 +6,7 @@
"@material-ui/core": "^3.9.0",
"@material-ui/icons": "^3.0.2",
"@navjobs/upload": "^3.1.3",
"autosuggest-highlight": "^3.1.1",
"base-64": "^0.1.0",
"chroma-js": "^2.0.3",
"d3": "^5.9.1",
......@@ -13,7 +14,9 @@
"file-saver": "^2.0.0",
"html-to-react": "^1.3.3",
"marked": "^0.6.0",
"material-ui-chip-input": "^1.0.0-beta.14",
"react": "^16.4.2",
"react-autosuggest": "^9.4.3",
"react-cookie": "^3.0.8",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.4.2",
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import ChipInput from 'material-ui-chip-input'
import Autosuggest from 'react-autosuggest'
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'
function renderInput(inputProps) {
const { classes, autoFocus, value, onChange, onAdd, onDelete, chips, ref, ...other } = inputProps
return (
<ChipInput
clearInputValueOnChange
onUpdateInput={onChange}
onAdd={onAdd}
onDelete={onDelete}
value={chips}
inputRef={ref}
{...other}
/>
)
}
function renderSuggestion(suggestion, { query, isHighlighted }) {
const matches = match(getSuggestionValue(suggestion), query)
const parts = parse(getSuggestionValue(suggestion), matches)
return (
<MenuItem
selected={isHighlighted}
component='div'
onMouseDown={(e) => e.preventDefault()} // prevent the click causing the input to be blurred
>
<div>
{parts.map((part, index) => {
return part.highlight ? (
<span key={String(index)} style={{ fontWeight: 300 }}>
{part.text}
</span>
) : (
<strong key={String(index)} style={{ fontWeight: 500 }}>
{part.text}
</strong>
)
})}
</div>
</MenuItem>
)
}
function renderSuggestionsContainer(options) {
const { containerProps, children } = options
return (
<Paper {...containerProps} square>
{children}
</Paper>
)
}
function getSuggestionValue(suggestion) {
return `${suggestion.key}=${suggestion.value}`
}
class SearchBar extends React.Component {
static styles = theme => ({
root: {
height: 250,
flexGrow: 1
},
container: {
position: 'relative'
},
suggestionsContainerOpen: {
position: 'absolute',
zIndex: 100,
marginTop: theme.spacing.unit,
left: 0,
right: 0
},
suggestion: {
display: 'block'
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
divider: {
height: theme.spacing.unit * 2
},
textField: {
width: '100%'
}
})
static propTypes = {
classes: PropTypes.object.isRequired,
aggregations: PropTypes.object.isRequired,
values: PropTypes.object.isRequired,
onChanged: PropTypes.func.isRequired
}
state = {
suggestions: [],
textFieldInput: ''
}
getSuggestions(value) {
value = value.toLowerCase()
const { aggregations } = this.props
const suggestions = []
Object.keys(aggregations).forEach(aggKey => {
Object.keys(aggregations[aggKey]).forEach(aggValue => {
if (aggValue.toLowerCase().startsWith(value)) {
suggestions.push({
key: aggKey,
value: aggValue
})
}
})
})
return suggestions
}
handleSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
})
};
handleSuggestionsClearRequested = () => {
this.setState({
suggestions: []
})
};
handleTextFieldInputChange = (event, { newValue }) => {
this.setState({
textFieldInput: newValue
})
};
handleAddChip(chip) {
const values = {...this.props.values}
let key, value
if (chip.includes('=')) {
const parts = chip.split('=')
key = parts[0]
value = parts[1]
} else {
const suggestion = this.getSuggestions(chip)[0]
key = suggestion.key
value = suggestion.value
}
if (values[key]) {
values[key] = key === 'atoms' ? [...values[key], value] : value
} else {
values[key] = key === 'atoms' ? [value] : value
}
this.setState({
textFieldInput: ''
})
this.props.onChanged(values)
}
handleBeforeAddChip(chip) {
const suggestions = this.getSuggestions(chip)
if (suggestions.length > 0) {
return true
} else {
return false
}
}
handleDeleteChip(chip) {
if (!chip) {
return
}
const parts = chip.split('=')
const key = parts[0]
const values = {...this.props.values}
delete values[key]
this.props.onChanged(values)
}
getChips() {
const values = {...this.props.values}
return Object.keys(values).filter(key => values[key]).map(key => {
if (key === 'atoms') {
return `atoms=[${values[key].join(',')}]`
} else {
return `${key}=${values[key]}`
}
})
}
render() {
const { classes, values, onChanged, ...rest } = this.props
return (
<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
}}
/>
)
}
}
export default withStyles(SearchBar.styles)(SearchBar)
......@@ -12,12 +12,12 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'
import ListItemText from '@material-ui/core/ListItemText'
import BackupIcon from '@material-ui/icons/Backup'
import SearchIcon from '@material-ui/icons/Search'
import AccountIcon from '@material-ui/icons/AccountCircle'
// import AccountIcon from '@material-ui/icons/AccountCircle'
import DocumentationIcon from '@material-ui/icons/Help'
import HomeIcon from '@material-ui/icons/Home'
import ArchiveIcon from '@material-ui/icons/Storage'
import EncIcon from '@material-ui/icons/Assessment'
import AnalyticsIcon from '@material-ui/icons/Settings'
// import ArchiveIcon from '@material-ui/icons/Storage'
// import EncIcon from '@material-ui/icons/Assessment'
// import AnalyticsIcon from '@material-ui/icons/Settings'
import DevelIcon from '@material-ui/icons/ReportProblem'
import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import MenuIcon from '@material-ui/icons/Menu'
......@@ -33,7 +33,7 @@ const drawerWidth = 200
const toolbarTitles = {
'/': 'Welcome',
'/repo': 'Raw Code Outputs',
'/repo': 'Search',
'/upload': 'Upload Your Own Data',
'/profile': 'Your Profile',
'/docs': 'Documentation',
......@@ -210,7 +210,7 @@ class Navigation extends React.Component {
<ListItemIcon>
<SearchIcon style={{fill: repoTheme.palette.primary.main}}/>
</ListItemIcon>
<ListItemText inset primary="Repository"/>
<ListItemText inset primary="Search"/>
</MenuItem>
<MenuItem className={classes.menuItem} component={Link} to="/upload" selected={ pathname === '/upload' }>
<ListItemIcon>
......@@ -218,7 +218,7 @@ class Navigation extends React.Component {
</ListItemIcon>
<ListItemText inset primary="Upload"/>
</MenuItem>
<MenuItem className={classes.menuItem} component={Link} to="/archive" selected={ pathname.startsWith('/archive') }>
{/* <MenuItem className={classes.menuItem} component={Link} to="/archive" selected={ pathname.startsWith('/archive') }>
<ListItemIcon>
<ArchiveIcon style={{fill: archiveTheme.palette.primary.main}}/>
</ListItemIcon>
......@@ -235,16 +235,16 @@ class Navigation extends React.Component {
<AnalyticsIcon style={{fill: analyticsTheme.palette.primary.main}}/>
</ListItemIcon>
<ListItemText inset primary="Analytics"/>
</MenuItem>
</MenuItem> */}
</MenuList>
<Divider/>
<MenuList>
<MenuItem className={classes.menuItem} component={Link} to="/profile" selected={ pathname === '/profile' }>
{/* <MenuItem className={classes.menuItem} component={Link} to="/profile" selected={ pathname === '/profile' }>
<ListItemIcon>
<AccountIcon />
</ListItemIcon>
<ListItemText inset primary="Profil"/>
</MenuItem>
</MenuItem> */}
<MenuItem className={classes.menuItem} component={Link} to="/docs" selected={ pathname === '/docs' }>
<ListItemIcon>
<DocumentationIcon />
......
......@@ -110,7 +110,8 @@ class PeriodicTable extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
aggregations: PropTypes.object,
onSelectionChanged: PropTypes.func.isRequired
values: PropTypes.array.isRequired,
onChanged: PropTypes.func.isRequired
}
static styles = theme => ({
......@@ -127,36 +128,29 @@ class PeriodicTable extends React.Component {
}
})
state = {
selected: []
}
onElementClicked(element) {
const index = this.state.selected.indexOf(element)
const index = this.props.values.indexOf(element)
const isClicked = index >= 0
let selected
if (isClicked) {
selected = [...this.state.selected]
selected = [...this.props.values]
selected.splice(index, 1)
this.setState({selected: selected})
} else {
selected = [element, ...this.state.selected]
this.setState({selected: selected})
selected = [element, ...this.props.values]
}
this.props.onSelectionChanged(selected)
this.props.onChanged(selected)
}
unSelectedAggregations() {
const { aggregations } = this.props
const { selected } = this.state
const { aggregations, values } = this.props
return Object.keys(aggregations)
.filter(key => selected.indexOf(key) === -1)
.filter(key => values.indexOf(key) === -1)
.map(key => aggregations[key])
}
render() {
const {classes, aggregations} = this.props
const {classes, aggregations, values} = this.props
const max = aggregations ? Math.max(...this.unSelectedAggregations()) || 1 : 1
const heatmapScale = chroma.scale(['#ffcdd2', '#d50000']).domain([1, max], 10, 'log')
return (
......@@ -174,7 +168,7 @@ class PeriodicTable extends React.Component {
heatmapScale={heatmapScale}
relativeCount={aggregations ? (aggregations[element.symbol] || 0) / max : 0}
onClick={() => this.onElementClicked(element.symbol)}
selected={this.state.selected.indexOf(element.symbol) >= 0}
selected={values.indexOf(element.symbol) >= 0}
/> : ''}
</td>
))}
......
......@@ -13,7 +13,8 @@ class QuantityHistogram extends React.Component {
title: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
data: PropTypes.object,
onSelectionChanged: PropTypes.func.isRequired
value: PropTypes.string,
onChanged: PropTypes.func.isRequired
}
static styles = theme => ({
......@@ -32,7 +33,6 @@ class QuantityHistogram extends React.Component {
}
state = {
selected: undefined,
scalePower: 0.25
}
......@@ -45,16 +45,11 @@ class QuantityHistogram extends React.Component {
}
handleItemClicked(item) {
const isSelected = this.state.selected === item.name
let selected
if (isSelected) {
selected = undefined
if (this.props.value === item.name) {
this.props.onChanged(null)
} else {
selected = item.name
this.props.onChanged(item.name)
}
this.setState({selected: selected})
this.props.onSelectionChanged(selected)
}
updateChart() {
......@@ -62,7 +57,8 @@ class QuantityHistogram extends React.Component {
return
}
const { selected, scalePower } = this.state
const { scalePower } = this.state
const selected = this.props.value
const width = this.container.current.offsetWidth
const height = Object.keys(this.props.data).length * 32
......
......@@ -19,6 +19,7 @@ import CalcDialog from './CalcDialog'
import PeriodicTable from './PeriodicTable'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import QuantityHistogram from './QuantityHistogram'
import SearchBar from '../SearchBar'
class Repo extends React.Component {
static propTypes = {
......@@ -77,6 +78,12 @@ class Repo extends React.Component {
whiteSpace: 'nowrap',
width: '100%',
maxWidth: 200
},
searchBarContainer: {
width: '100%',
maxWidth: 900,
margin: 'auto',
marginBottom: theme.spacing.unit * 3
}
})
......@@ -135,15 +142,15 @@ class Repo extends React.Component {
total: 0,
loading: true,
owner: 'all',
atoms: undefined,
sortedBy: 'formula',
sortOrder: 'asc',
openCalc: null
openCalc: null,
searchValues: {}
}
update(changes) {
changes = changes || {}
const { page, rowsPerPage, owner, sortedBy, sortOrder, atoms, system, crystal_system, code_name, xc_functional, basis_set } = {...this.state, ...changes}
const { page, rowsPerPage, owner, sortedBy, sortOrder, searchValues } = {...this.state, ...changes}
this.setState({loading: true, ...changes})
this.props.api.search({
......@@ -152,12 +159,7 @@ class Repo extends React.Component {
owner: owner || 'all',
order_by: sortedBy,
order: (sortOrder === 'asc') ? 1 : -1,
atoms: atoms,
system: system,
crystal_system: crystal_system,
code_name: code_name,
xc_functional: xc_functional,
basis_set: basis_set
...searchValues
}).then(data => {
const { pagination: { total, page, per_page }, results, aggregations } = data
this.setState({
......@@ -209,17 +211,27 @@ class Repo extends React.Component {
this.setState({openCalc: calc_id})
}
handleElementSelectionChanged(selection) {
if (selection.length === 0) {
selection = undefined
handleAtomsChanged(atoms) {
const searchValues = {...this.state.searchValues}
searchValues.atoms = atoms
if (searchValues.atoms.length === 0) {
delete searchValues.atoms
}
this.update({searchValues: searchValues})
}
handleQuantityChanged(quantity, selection) {
const searchValues = {...this.state.searchValues}
if (selection) {
searchValues[quantity] = selection
} else {
delete searchValues[quantity]
}
this.update({atoms: selection})
this.update({searchValues: searchValues})
}
handleQuantitySelectionChanged(quantity, selection) {
const update = {}
update[quantity] = selection
this.update(update)
handleSearchChanged(searchValues) {
this.update({searchValues: searchValues})
}
renderCell(key, rowConfig, calc) {
......@@ -237,7 +249,7 @@ class Repo extends React.Component {
render() {
const { classes, user } = this.props
const { data, rowsPerPage, page, total, loading, sortedBy, sortOrder, openCalc } = this.state
const { data, rowsPerPage, page, total, loading, sortedBy, sortOrder, openCalc, searchValues } = this.state
const emptyRows = rowsPerPage - Math.min(rowsPerPage, total - (page - 1) * rowsPerPage)
const aggregations = this.state.aggregations || {}
......@@ -245,7 +257,8 @@ class Repo extends React.Component {
const quantity = (key, title) => (<QuantityHistogram
classes={{root: classes.quantity}} title={title || key} width={300}
data={aggregations[key]}
onSelectionChanged={(selection) => this.handleQuantitySelectionChanged(key, selection)}/>)
value={searchValues[key]}
onChanged={(selection) => this.handleQuantityChanged(key, selection)}/>)
const ownerLabel = {
all: 'All calculations',
......@@ -255,7 +268,7 @@ class Repo extends React.Component {
return (
<div className={classes.root}>
{ openCalc ? <CalcDialog calcId={openCalc.calc_id} uploadId={openCalc.upload_id} onClose={() => this.handleCalcClose()} /> : ''}
<Typography variant="h4" className={classes.title}>The Repository Raw Code Data</Typography>
{ user
? <FormControl>
<FormLabel>Filter calculations and only show: </FormLabel>
......@@ -272,14 +285,23 @@ class Repo extends React.Component {
</FormControl> : ''
}
<div className={classes.searchBarContainer}>
<SearchBar
fullWidth fullWidthInput={false} label="search" placeholder="enter atoms or other quantities"
aggregations={aggregations} values={searchValues}
onChanged={values => this.handleSearchChanged(values)}
/>
</div>
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} className={classes.searchSummary}>
<Typography>Search</Typography>
<Typography variant="h6" style={{textAlign: 'center', width: '100%'}}>found {total} code runs</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.searchDetails}>
<PeriodicTable
aggregations={aggregations ? aggregations.atoms : null}
onSelectionChanged={(selection) => this.handleElementSelectionChanged(selection)}
aggregations={aggregations.atoms}
values={searchValues.atoms || []}
onChanged={(selection) => this.handleAtomsChanged(selection)}
/>
<Grid container spacing={24} className={classes.quantityGrid}>
......@@ -303,7 +325,7 @@ class Repo extends React.Component {
</FormLabel>
<FormLabel classes={{root: classes.selectLabel}}>
Analyse {total} calculations in an analytics notebook
Analyse {total} code runs in an analytics notebook
</FormLabel>
<MuiThemeProvider theme={analyticsTheme}>
<IconButton color="primary" component={Link} to={`/analytics`}>
......
......@@ -2,9 +2,7 @@ import React from 'react'
import PropTypes, { instanceOf } from 'prop-types'
import Markdown from './Markdown'
import { withStyles, Paper, IconButton, FormGroup, Checkbox, FormControlLabel, FormLabel,
LinearProgress,
Typography,
Tooltip} from '@material-ui/core'
LinearProgress, Tooltip} from '@material-ui/core'
import UploadIcon from '@material-ui/icons/CloudUpload'
import Dropzone from 'react-dropzone'
import Upload from './Upload'
......@@ -261,7 +259,6 @@ class Uploads extends React.Component {
return (
<div className={classes.root}>
<Typography variant="h4">Upload your own data</Typography>