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

Fixed most issues.

parent 2b2f02a1
......@@ -5,4 +5,6 @@ echo log, ref, version, commit = \"$(git log -1 --oneline)\", \"$(git describe -
# gui
commit=`git rev-parse --short --verify HEAD`
sed -i -e "s/nomad-gui-commit-placeholder/$commit/g" gui/package.json
rm -f gui/package.json-e
\ No newline at end of file
rm -f gui/package.json-e
python nomad/search.py > gui/src/searchQuantities.json
\ No newline at end of file
import React, { useContext, useState, useEffect } from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import { compose } from 'recompose'
import { withErrors, errorContext } from './errors'
import { withApi, apiContext } from './api'
import { errorContext } from './errors'
import { apiContext } from './api'
import Search from './search/Search'
import { Typography, makeStyles } from '@material-ui/core'
import { DatasetActions, DOI } from './search/DatasetList'
......@@ -92,16 +89,8 @@ export default function DatasetPage() {
initialQuery={{owner: 'all'}}
query={{dataset_id: datasetId}}
ownerTypes={['all', 'public']}
initialResultTab="entries" availableResultTabs={['entries', 'groups', 'datasets']}
initialResultTab="entries"
availableResultTabs={['entries', 'groups', 'datasets']}
/>
</div>
}
DatasetPage.propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
history: PropTypes.object.isRequired
}
......@@ -18,6 +18,7 @@ import SearchIcon from '@material-ui/icons/Search'
import UploadsChart from './UploadsChart'
import { Quantity } from './QuantityHistogram'
import SearchContext, { searchContext } from './SearchContext'
import {objectFilter} from '../../utils'
const resultTabs = {
'entries': {
......@@ -60,6 +61,10 @@ const useSearchStyles = makeStyles(theme => ({
padding: theme.spacing(3)
}
}))
/**
* This component shows the full search interface including result lists.
*/
export default function Search(props) {
const {
initialVisualizationTab,
......@@ -69,10 +74,14 @@ export default function Search(props) {
initialMetric,
initialResultTab,
availableResultTabs,
query,
initialQuery,
resultListProps,
initialRequest,
...rest} = props
const classes = useSearchStyles()
return <DisableOnLoading>
<SearchContext>
<SearchContext query={query} initialQuery={initialQuery}>
<div className={classes.root} {...rest}>
<SearchEntry
initialTab={initialVisualizationTab}
......@@ -80,10 +89,12 @@ export default function Search(props) {
ownerTypes={ownerTypes}
initialDomain={initialDomain}
initialMetric={initialMetric}
initialRequest={initialRequest}
/>
<SearchResults
initialTab={initialResultTab}
availableTabs={availableResultTabs}
resultListProps={resultListProps}
/>
</div>
</SearchContext>
......@@ -96,7 +107,20 @@ Search.propTypes = {
initialOwner: PropTypes.string,
ownerTypes: PropTypes.arrayOf(PropTypes.string),
initialDomain: PropTypes.string,
initialMetric: PropTypes.string
initialMetric: PropTypes.string,
initialRequest: PropTypes.object,
resultListProps: PropTypes.object,
/**
* Additional search parameters that will be added to all searches that are send to
* the API. The idea is that this can be used to lock some aspects of the search for
* special contexts, like the dataset page for example.
*/
query: PropTypes.object,
/**
* Similar to query, but these parameters can be changes by the user interacting with
* the component.
*/
initialQuery: PropTypes.object
}
const useSearchEntryStyles = makeStyles(theme => ({
......@@ -384,7 +408,7 @@ const ownerLabel = {
const ownerTooltips = {
all: 'This will show all entries in the database.',
visible: 'Do also show entries that are only visible to you.',
public: 'Do not entries with embargo.',
public: 'Do not show entries with embargo.',
user: 'Do only show entries visible to you.',
staging: 'Will only show entries that you uploaded, but not yet published.'
}
......@@ -438,7 +462,7 @@ OwnerSelect.propTypes = {
const useSearchResultStyles = makeStyles(theme => ({
root: theme.spacing(4)
}))
function SearchResults({availableTabs = ['entries'], initialTab = 'entries'}) {
function SearchResults({availableTabs = ['entries'], initialTab = 'entries', resultListProps = {}}) {
const classes = useSearchResultStyles()
const {domain, setGroups} = useContext(searchContext)
let [openTab, setOpenTab] = useQueryParam('results', StringParam)
......@@ -469,13 +493,14 @@ function SearchResults({availableTabs = ['entries'], initialTab = 'entries'}) {
})}
</Tabs>
<ResultList domain={domain} />
<ResultList domain={domain} {...resultListProps} />
</Paper>
</div>
}
SearchResults.propTypes = {
'availableTabs': PropTypes.arrayOf(PropTypes.string),
'initialTab': PropTypes.string
'initialTab': PropTypes.string,
'resultListProps': PropTypes.object
}
function ReRunSearchButton() {
......@@ -489,9 +514,10 @@ function ReRunSearchButton() {
const usePagination = () => {
const {setRequestParameters} = useContext(searchContext)
const [requestQueryParameters, setRequestQueryParameters] = useQueryParams({
let [requestQueryParameters, setRequestQueryParameters] = useQueryParams({
order: NumberParam, order_by: StringParam, per_page: NumberParam, page: NumberParam
})
requestQueryParameters = objectFilter(requestQueryParameters, key => requestQueryParameters[key])
useEffect(
() => setRequestParameters(requestQueryParameters),
[requestQueryParameters, setRequestParameters]
......@@ -524,11 +550,11 @@ const useScroll = (apiGroupName, afterParameterName) => {
}
function SearchEntryList(props) {
const {response, query} = useContext(searchContext)
const {response, requestParameters, apiQuery} = useContext(searchContext)
const setRequestParameters = usePagination()
return <EntryList
query={query}
editable={query.owner === 'staging' || query.owner === 'user'}
query={apiQuery}
editable={apiQuery.owner === 'staging' || apiQuery.owner === 'user'}
data={response}
onChange={setRequestParameters}
actions={
......@@ -537,7 +563,7 @@ function SearchEntryList(props) {
<ApiDialogButton data={response} />
</React.Fragment>
}
{...response.pagination}
{...requestParameters}
{...props}
/>
}
......
......@@ -246,14 +246,13 @@ class SearchBar extends React.Component {
}
handleClear() {
const {query, setQuery} = this.context
const values = {owner: query.owner}
setQuery(values, true)
const {setQuery} = this.context
setQuery({}, true)
}
getChips() {
const {query: {owner, domain, ...values}} = this.context
const domainPrefix = domain + '.'
const {query: values, domain} = this.context
const domainPrefix = domain.key + '.'
return Object.keys(values).filter(key => values[key]).map(key => {
if (key === 'atoms') {
return `atoms=[${values[key].join(',')}]`
......
......@@ -2,9 +2,33 @@ import React, { useState, useContext, useEffect, useRef, useCallback } from 'rea
import PropTypes from 'prop-types'
import hash from 'object-hash'
import { errorContext } from '../errors'
import { onlyUnique } from '../../utils'
import { onlyUnique, objectFilter } from '../../utils'
import { domains } from '../domains'
import { apiContext } from '../api'
import { useLocation, useHistory } from 'react-router-dom'
import qs from 'qs'
import * as searchQuantities from '../../searchQuantities.json'
const useSearchUrlQuery = () => {
const location = useLocation()
const history = useHistory()
const urlQuery = location.search ? {
...qs.parse(location.search.substring(1))
} : {}
const searchQuery = objectFilter(urlQuery, key => searchQuantities[key])
const rest = objectFilter(urlQuery, key => !searchQuantities[key])
if (searchQuery.atoms && !Array.isArray(searchQuery.atoms)) {
searchQuery.atoms = [searchQuery.atoms]
}
if (searchQuery.only_atoms && !Array.isArray(searchQuery.only_atoms)) {
searchQuery.only_atoms = [searchQuery.only_atoms]
}
const setUrlQuery = query => history.push(location.pathname + '?' + qs.stringify({
...rest,
...query
}, {indices: false}))
return [searchQuery, setUrlQuery]
}
/**
* The React context object. Can be accessed from functional components with useContext.
......@@ -21,7 +45,7 @@ export const searchContext = React.createContext()
* pagination, statistics, order. The query object contains all parameters that
* constitute the actual search. This includes the domain and owner parameters.
*/
export default function SearchContext({initialRequest, initialQuery, children}) {
export default function SearchContext({initialRequest, initialQuery, query, children}) {
const defaultStatistics = ['atoms', 'authors']
const emptyResponse = {
statistics: {
......@@ -32,14 +56,18 @@ export default function SearchContext({initialRequest, initialQuery, children})
pagination: {
total: undefined,
per_page: 10,
page: 1
page: 1,
order: -1,
order_by: 'upload_time'
},
metric: domains.dft.defaultSearchMetric
}
const {api, info} = useContext(apiContext)
const {api} = useContext(apiContext)
const {raiseError} = useContext(errorContext)
const [urlQuery, setUrlQuery] = useSearchUrlQuery()
// React calls the children effects for the parent effect. But the parent effect is
// run with the state of the last render, which is the state before the children effects.
// If we would maintain the request in regular React state, we might execute unnecessary
......@@ -62,15 +90,14 @@ export default function SearchContext({initialRequest, initialQuery, children})
statistics: [],
groups: {},
domainKey: domains.dft.key,
owner: 'all',
pagination: {
order_by: 'upload_id',
order: -1,
page: 1,
per_page: 10
},
query: {
owner: 'all'
per_page: 10,
order: -1,
order_by: 'upload_time'
},
query: {},
update: 0
})
const requestHashRef = useRef(0)
......@@ -84,7 +111,7 @@ export default function SearchContext({initialRequest, initialQuery, children})
// checks for necessity. It will update the response state, once the request has
// been answered by the api.
const runRequest = useCallback(() => {
const {metric, domainKey} = requestRef.current
const {metric, domainKey, owner} = requestRef.current
const domain = domains[domainKey]
const apiRequest = {
...initialRequest,
......@@ -96,8 +123,10 @@ export default function SearchContext({initialRequest, initialQuery, children})
}
const apiQuery = {
...apiRequest,
owner: owner,
...initialQuery,
...requestRef.current.query
...requestRef.current.query,
...query
}
api.search(apiQuery, statisticsToRefresh)
.then(newResponse => {
......@@ -142,7 +171,7 @@ export default function SearchContext({initialRequest, initialQuery, children})
}, [onRequestChange, requestRef])
const setOwner = useCallback(owner => {
requestRef.current.query.owner = owner
requestRef.current.owner = owner
onRequestChange()
}, [onRequestChange, requestRef])
......@@ -173,13 +202,18 @@ export default function SearchContext({initialRequest, initialQuery, children})
}
if (replace) {
requestRef.current.query = {...changes}
setUrlQuery(changes)
} else {
requestRef.current.query = {...requestRef.current.query, ...changes}
setUrlQuery({...urlQuery, ...changes})
}
onRequestChange()
}
useEffect(() => {
requestRef.current.query = urlQuery
onRequestChange()
}, [urlQuery, requestRef, onRequestChange])
// We initially trigger a search request on mount.
useEffect(() => {
// In some cases, especially on mount, requestHash might not be based on the
......@@ -194,11 +228,17 @@ export default function SearchContext({initialRequest, initialQuery, children})
const value = {
response: response,
query: {
domain: requestRef.current.domainKey,
...requestRef.current.query
},
apiQuery: {
domain: requestRef.current.domainKey,
owner: requestRef.current.owner,
...requestRef.current.query,
...query
},
domain: domains[requestRef.current.domainKey],
metric: requestRef.current.metric,
requestParameters: requestRef.current.pagination,
setRequestParameters: setRequestParameters,
setQuery: handleQueryChange,
setMetric: setMetric,
......@@ -213,6 +253,11 @@ export default function SearchContext({initialRequest, initialQuery, children})
return <searchContext.Provider value={value} >{children}</searchContext.Provider>
}
SearchContext.propTypes = {
/**
* An object with initial query parameters. These will be added to the search context
* and be used in all search requests.
*/
query: PropTypes.object,
/**
* An object with initial query parameters. These will be added to the search context
* and the first search request. Afterwards search parameters might be removed or
......
......@@ -62,12 +62,13 @@ export default function SearchPage() {
}
}
const withoutLogin = ['all']
const withoutLogin = ['all', 'public']
return <Search
initialQuery={query}
initialVisualizationTab="elements"
availableResultTabs={['entries', 'groups', 'datasets']}
initialOwner="public"
ownerTypes={['public', 'visible'].filter(key => user || withoutLogin.indexOf(key) !== -1)}
/>
}
{
"ems.chemical": {
"name": "ems.chemical",
"description": null,
"many": false
},
"ems.sample_constituents": {
"name": "ems.sample_constituents",
"description": null,
"many": false
},
"ems.sample_microstructure": {
"name": "ems.sample_microstructure",
"description": null,
"many": false
},
"ems.experiment_summary": {
"name": "ems.experiment_summary",
"description": null,
"many": false
},
"ems.experiment_location": {
"name": "ems.experiment_location",
"description": null,
"many": false
},
"ems.experiment_time": {
"name": "ems.experiment_time",
"description": null,
"many": false
},
"ems.method": {
"name": "ems.method",
"description": null,
"many": false
},
"ems.probing_method": {
"name": "ems.probing_method",
"description": null,
"many": false
},
"ems.repository_name": {
"name": "ems.repository_name",
"description": null,
"many": false
},
"ems.repository_url": {
"name": "ems.repository_url",
"description": null,
"many": false
},
"ems.entry_repository_url": {
"name": "ems.entry_repository_url",
"description": null,
"many": false
},
"ems.preview_url": {
"name": "ems.preview_url",
"description": null,
"many": false
},
"ems.quantities": {
"name": "ems.quantities",
"description": null,
"many": false
},
"ems.group_hash": {
"name": "ems.group_hash",
"description": null,
"many": false
},
"labels.label": {
"name": "labels.label",
"description": null,
"many": false
},
"labels.type": {
"name": "labels.type",
"description": null,
"many": false
},
"labels.source": {
"name": "labels.source",
"description": null,
"many": false
},
"optimade.elements": {
"name": "optimade.elements",
"description": "Names of the different elements present in the structure.",
"many": false
},
"optimade.nelements": {
"name": "optimade.nelements",
"description": "Number of different elements in the structure as an integer.",
"many": false
},
"optimade.elements_ratios": {
"name": "optimade.elements_ratios",
"description": "Relative proportions of different elements in the structure.",
"many": false
},
"optimade.chemical_formula_descriptive": {
"name": "optimade.chemical_formula_descriptive",
"description": "The chemical formula for a structure as a string in a form chosen by the API\nimplementation.",
"many": false
},
"optimade.chemical_formula_reduced": {
"name": "optimade.chemical_formula_reduced",
"description": "The reduced chemical formula for a structure as a string with element symbols and\ninteger chemical proportion numbers. The proportion number MUST be omitted if it is 1.",
"many": false
},
"optimade.chemical_formula_hill": {
"name": "optimade.chemical_formula_hill",
"description": "The chemical formula for a structure in Hill form with element symbols followed by\ninteger chemical proportion numbers. The proportion number MUST be omitted if it is 1.",
"many": false
},
"optimade.chemical_formula_anonymous": {
"name": "optimade.chemical_formula_anonymous",
"description": "The anonymous formula is the chemical_formula_reduced, but where the elements are\ninstead first ordered by their chemical proportion number, and then, in order left to\nright, replaced by anonymous symbols A, B, C, ..., Z, Aa, Ba, ..., Za, Ab, Bb, ... and\nso on.",
"many": false
},
"optimade.dimension_types": {
"name": "optimade.dimension_types",
"description": "List of three integers. For each of the three directions indicated by the three lattice\nvectors (see property lattice_vectors). This list indicates if the direction is\nperiodic (value 1) or non-periodic (value 0). Note: the elements in this list each\nrefer to the direction of the corresponding entry in lattice_vectors and not\nthe Cartesian x, y, z directions.",
"many": false
},
"optimade.nsites": {
"name": "optimade.nsites",
"description": "An integer specifying the length of the cartesian_site_positions property.",
"many": false
},
"optimade.structure_features": {
"name": "optimade.structure_features",
"description": "A list of strings that flag which special features are used by the structure.\n\n- disorder: This flag MUST be present if any one entry in the species list has a\nchemical_symbols list that is longer than 1 element.\n- unknown_positions: This flag MUST be present if at least one component of the\ncartesian_site_positions list of lists has value null.\n- assemblies: This flag MUST be present if the assemblies list is present.",
"many": false
},
"dft.basis_set": {
"name": "dft.basis_set",
"description": "The used basis set functions.",
"many": false
},
"dft.xc_functional": {
"name": "dft.xc_functional",
"description": "The libXC based xc functional classification used in the simulation.",
"many": false
},
"dft.system": {
"name": "dft.system",
"description": "The system type of the simulated system.",
"many": false
},
"dft.compound_type": {
"name": "dft.compound_type",
"description": "The compound type of the simulated system.",
"many": false
},
"dft.crystal_system": {
"name": "dft.crystal_system",
"description": "The crystal system type of the simulated system.",
"many": false
},
"dft.spacegroup": {
"name": "dft.spacegroup",
"description": "The spacegroup of the simulated system as number.",
"many": false
},
"dft.spacegroup_symbol": {
"name": "dft.spacegroup_symbol",
"description": "The spacegroup as international short symbol.",
"many": false
},
"dft.code_name": {
"name": "dft.code_name",
"description": "The name of the used code.",
"many": false
},
"dft.code_version": {
"name": "dft.code_version",
"description": "The version of the used code.",
"many": false
},
"dft.n_geometries": {
"name": "dft.n_geometries",
"description": "Number of unique geometries.",
"many": false
},
"dft.n_calculations": {
"name": "dft.n_calculations",
"description": "Number of single configuration calculation sections",
"many": false
},
"dft.n_total_energies": {
"name": "dft.n_total_energies",
"description": "Number of total energy calculations",
"many": false
},
"dft.n_quantities": {
"name": "dft.n_quantities",
"description": "Number of metainfo quantities parsed from the entry.",
"many": false
},
"dft.quantities": {
"name": "dft.quantities",
"description": "All quantities that are used by this entry.",
"many": true
},
"dft.searchable_quantities": {
"name": "dft.searchable_quantities",
"description": "Energy-related quantities.",
"many": true
},
"dft.geometries": {
"name": "dft.geometries",
"description": "Hashes for each simulated geometry",
"many": false
},
"dft.group_hash": {
"name": "dft.group_hash",
"description": "Hashes that describe unique geometries simulated by this code run.",
"many": true
},
"dft.labels_springer_compound_class": {
"name": "dft.labels_springer_compound_class",
"description": "Springer compund classification.",
"many": true
},