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

Merge branch '721-workflow-search' into 'v1.0.0'

Workflow search

See merge request !531
parents 21535fc6 a539a500
Pipeline #120170 canceled with stages
in 5 minutes and 55 seconds
......@@ -94,4 +94,4 @@ is added to the quantity path, e.g. `mainfile.path`.
## The search web interface
Comming soon ...
\ No newline at end of file
Comming soon ...
......@@ -35,12 +35,13 @@ import { Link as RouterLink } from 'react-router-dom'
import { DOI } from './dataset/DOI'
import ClipboardIcon from '@material-ui/icons/Assignment'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import { get } from 'lodash'
import { get, isNil } from 'lodash'
import searchQuantities from '../searchQuantities'
import Placeholder from './visualization/Placeholder'
import NoData from './visualization/NoData'
import { formatNumber, formatTimestamp, authorList, serializeMetainfo } from '../utils'
import { Unit, toUnitSystem, useUnits } from '../units'
import { filterData } from './search/FilterRegistry'
/**
* Component for showing a metainfo quantity value together with a name and
......@@ -117,6 +118,7 @@ const Quantity = React.memo((props) => {
quantity,
label,
description,
value,
loading,
placeholder,
typography,
......@@ -142,43 +144,46 @@ const Quantity = React.memo((props) => {
}
// Determine the final value to show.
let value
if (!loading) {
if (typeof quantity === 'string') {
value = data && quantity && get(data, quantity)
if (format) {
value = serializeMetainfo(quantity, value, units)
}
} else if (children) {
} else {
try {
value = quantity(data)
} catch {
value = undefined
let finalValue = value
if (isNil(value)) {
if (typeof quantity === 'string') {
finalValue = data && quantity && get(data, quantity)
} else if (children) {
} else {
try {
finalValue = quantity(data)
} catch {
finalValue = undefined
}
}
}
if (value === 'not processed') {
value = 'unavailable'
if (finalValue === 'not processed') {
finalValue = 'unavailable'
}
if (value === 'unavailable') {
value = ''
if (finalValue === 'unavailable') {
finalValue = ''
}
if ((!value && !children) && hideIfUnavailable) {
if (format) {
finalValue = serializeMetainfo(quantity, finalValue, units)
}
if ((!finalValue && !children) && hideIfUnavailable) {
return null
}
if (children && children.length !== 0) {
content = children
} else if (value || value === 0) {
if (Array.isArray(value)) {
value = value.join(', ')
} else if (finalValue || finalValue === 0) {
if (Array.isArray(finalValue)) {
finalValue = finalValue.join(', ')
}
clipboardContent = clipboardContent || value
clipboardContent = clipboardContent || finalValue
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
{value}
{finalValue}
</Typography>
} else {
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
......@@ -223,11 +228,12 @@ const Quantity = React.memo((props) => {
? <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>loading ...</i>
</Typography>
// The portal is disabled for this tooltip because the contents may
// contain links that cause a navigation that otherwise leaves the
// popup opened (the Tooltip state does not get updated since the
// page may be cached and a new page is shown immediately).
: <Tooltip title={tooltip} PopperProps={{disablePortal: true}}>
// The tooltip portal is disabled for custom contents that may
// contain links. Pressing a link while the tooltip is shown will
// cause a navigation that leaves the popup opened (the Tooltip
// state does not get updated since the page may be cached and a new
// page is shown immediately).
: <Tooltip title={tooltip} PopperProps={children ? {disablePortal: true} : undefined}>
{content}
</Tooltip>
}
......@@ -268,6 +274,7 @@ Quantity.propTypes = {
column: PropTypes.bool,
flex: PropTypes.bool,
data: PropTypes.object,
value: PropTypes.any,
quantity: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
......@@ -432,6 +439,8 @@ QuantityRow.propTypes = {
*/
export const QuantityCell = React.memo(({
quantity,
value,
data,
label,
description,
classes,
......@@ -439,22 +448,26 @@ export const QuantityCell = React.memo(({
children,
...other
}) => {
const data = useContext(quantityTableContext)
const contextData = useContext(quantityTableContext)
const finalData = data || contextData
return <TableCell align="left" {...other}>
{children || <Quantity
quantity={quantity}
value={value}
label={label}
description={description}
format
noWrap
data={data}
data={finalData}
/>}
</TableCell>
})
QuantityCell.propTypes = {
quantity: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
value: PropTypes.any,
data: PropTypes.object,
label: PropTypes.string,
description: PropTypes.string,
options: PropTypes.object,
......@@ -509,12 +522,13 @@ export const SectionTable = React.memo(({
</TableCell>}
{Object.keys(quantities).map((key, index) => {
const defCustom = quantities[key]
const def = searchQuantities[`${section}.${key}`]
const def = filterData[`${section}.${key}`]
const unitName = defCustom.unit || def?.unit
const unit = unitName && new Unit(unitName)
const unitLabel = unit && unit.label(units)
const label = defCustom.label || def?.label
const description = defCustom.description || def?.description || ''
const content = unit ? `${defCustom.label} (${unitLabel})` : defCustom.label
const content = unit ? `${label} (${unitLabel})` : defCustom.label
const align = defCustom.align || 'right'
return <TableCell key={index} align={align}>
<Tooltip title={description}>
......@@ -558,13 +572,14 @@ export const SectionTable = React.memo(({
<TableRow key={i}>
{data.data.map((row, j) => {
const defCustom = quantities[key]
const def = searchQuantities[`${section}.${key}`]
const def = filterData[`${section}.${key}`]
const unitName = defCustom.unit || def?.unit
const unit = unitName && new Unit(unitName)
const unitLabel = unit ? ` ${unit.label(units)}` : ''
const description = defCustom.description || def.description || ''
const description = defCustom.description || def?.description || ''
const dtype = defCustom?.type?.type_data || def?.type?.type_data
const align = defCustom.align || 'right'
const label = defCustom.label || def?.label
let value = row[key]
if (value !== undefined) {
if (!isNaN(value)) {
......@@ -580,7 +595,7 @@ export const SectionTable = React.memo(({
<TableCell key={j} align={align}>
<Tooltip title={description}>
<span>
{defCustom.label}
{label}
</span>
</Tooltip>
</TableCell>
......
......@@ -28,23 +28,33 @@ export default function GeometryOptimizationCard({index, archive, properties}) {
// Find out which properties are present
const hasGeometryOptimization = properties.has('geometry_optimization')
// Do not show the card if none of the properties are available
// Do not show the card if none of the properties are available, or if only
// one step is calculated.
if (!hasGeometryOptimization) {
return null
}
// Resolve geometry optimization data
let geometryOptimization = hasGeometryOptimization ? null : false
const geoOptProps = archive?.results?.properties?.geometry_optimization
const geoOptMethod = index.results.method?.simulation?.geometry_optimization
if (geoOptProps) {
geometryOptimization = {}
geometryOptimization.energies = resolveRef(geoOptProps.energies, archive)
geometryOptimization.convergence_tolerance_energy_difference = geoOptMethod?.convergence_tolerance_energy_difference
// Resolve energies
let energies = hasGeometryOptimization ? null : false
const geoOptPropsArchive = archive?.results?.properties?.geometry_optimization
if (hasGeometryOptimization && archive) {
energies = resolveRef(geoOptPropsArchive.energies, archive)
}
// Resolve convergence properties
let convergence = false
const geoOptProps = index?.results?.properties?.geometry_optimization
const geoOptMethod = index.results?.method?.simulation?.geometry_optimization
if (hasGeometryOptimization) {
convergence = {...geoOptMethod, ...geoOptProps}
}
return <PropertyCard title="Geometry optimization">
<GeometryOptimization data={geometryOptimization} units={units} />
<GeometryOptimization
energies={energies}
convergence={convergence}
units={units}
/>
</PropertyCard>
}
......
......@@ -198,7 +198,7 @@ const MaterialCard = React.memo(({index, properties, archive}) => {
<QuantityCell quantity={`${structurePath}.lattice_parameters.gamma`} label="γ"/>
</QuantityRow>
<QuantityRow>
<QuantityCell colSpan={3} quantity={`${structurePath}.cell_volume`}/>
<QuantityCell colSpan={2} quantity={`${structurePath}.cell_volume`}/>
</QuantityRow>
</QuantityTable>
: <NoData/>}
......
This diff is collapsed.
......@@ -45,7 +45,7 @@ import { useErrors } from '../errors'
import { combinePagination } from '../datatable/Datatable'
import { inputSectionContext } from './input/InputSection'
import {
filterDataGlobal,
filterData as filterDataGlobal,
filterAbbreviations,
filterFullnames,
materialNames,
......@@ -626,7 +626,7 @@ export const SearchContext = React.memo(({
const useAgg = (name, update = true, size = undefined, id = 'default') => {
const setAgg = useSetRecoilState(aggsFamily(name))
const aggResponse = useRecoilValue(aggsResponseFamily(name))
const aggSize = size || filterData[name].aggSize
const aggSize = size || filterData[name]?.aggDefaultSize
useEffect(() => {
setAgg(old => {
......@@ -1329,9 +1329,9 @@ export function toGUIFilterSingle(key, value, units = undefined, path = undefine
* API.
*
* @param {object} aggs The aggregation data as constructed by the GUI.
* @param {object} updatedFilters Set of filters that were updated together with
* this call.
* @param {string} resource The resource we are looking at: entries or materials.
* @param {bool} update Whether to force the update of aggregations, overriding
* the update-attribute of each aggregation.
*
* @returns {object} Aggregation query that is usable by the API.
*/
......@@ -1342,21 +1342,23 @@ function toAPIAgg(aggs, updatedFilters, resource) {
const agg = aggs[key]
const aggSet = filterDataGlobal[key].aggSet
if (aggSet) {
for (const [key, data] of Object.entries(aggSet)) {
for (const [quantity, data] of Object.entries(aggSet)) {
// If filter has been updated and the filter values are exclusive, the
// filter is excluded from the aggregation.
const type = data.type
const exclude = data.exclude
? data.exclude(updatedFilters)
: updatedFilters.has(key) && filterDataGlobal[key].exclusive
const name = resource === 'materials' ? materialNames[key.split(':')[0]] : key
const apiAgg = apiAggs[name] || {}
apiAgg[type] = {
quantity: name,
exclude_from_search: exclude,
size: agg.size
for (const [type, options] of Object.entries(data)) {
const exclude = options.exclude
? options.exclude(updatedFilters)
: updatedFilters.has(key) && filterDataGlobal[key].exclusive
const name = resource === 'materials' ? materialNames[quantity.split(':')[0]] : quantity
const apiAgg = apiAggs[name] || {}
apiAgg[type] = {
quantity: name,
exclude_from_search: exclude,
...options,
size: agg.size
}
apiAggs[name] = apiAgg
}
apiAggs[name] = apiAgg
}
}
}
......@@ -1443,13 +1445,6 @@ function reduceAggs(aggs, oldAggs, queryChanged) {
}
}
// Some aggregations require us to load more data than what we are currently
// showing (e.g. properties lists).
const sizeOverride = filterDataGlobal[key].aggSizeOverride
if (sizeOverride) {
size = sizeOverride
}
// If the query has not changed, see if there is an old aggregation which
// has a size that is at least as big as the currently requested size.
if (!queryChanged) {
......
......@@ -130,7 +130,7 @@ const InputField = React.memo(({
}, [initialSize, filterData, quantity, metainfoOptions])
const nFixedOptions = fixedOptions && Object.keys(fixedOptions).length
const minSize = disableOptions ? 0 : initialSize || nFixedOptions || filterData[quantity].aggSize
const minSize = disableOptions ? 0 : initialSize || nFixedOptions || filterData[quantity]?.aggDefaultSize
const [requestedAggSize, setRequestedAggSize] = useState(minSize)
const nMaxOptions = metainfoOptions && Object.keys(metainfoOptions).length
const incr = useState(increment || minSize)[0]
......
......@@ -25,13 +25,13 @@ import { isNil } from 'lodash'
import InputHeader from './InputHeader'
import InputTooltip from './InputTooltip'
import { InputTextField } from './InputText'
import { Quantity, Unit, toUnitSystem, toSI } from '../../../units'
import { Quantity, Unit, toUnitSystem, toSI, getDimension } from '../../../units'
import { formatNumber } from '../../../utils'
import searchQuantities from '../../../searchQuantities'
import { useSearchContext } from '../SearchContext'
function format(value) {
return formatNumber(value, 'float', 6, true)
return formatNumber(value, 'float', 3, true)
}
const useStyles = makeStyles(theme => ({
......@@ -45,9 +45,12 @@ const useStyles = makeStyles(theme => ({
},
textField: {
marginTop: 0,
marginBotton: 0,
marginBottom: 0,
flexGrow: 1,
width: '10rem'
width: '16rem'
},
textInput: {
textOverflow: 'ellipsis'
},
container: {
width: '100%'
......@@ -77,6 +80,7 @@ const InputSlider = React.memo(({
quantity,
description,
step,
nSteps,
visible,
className,
classes,
......@@ -248,6 +252,15 @@ const InputSlider = React.memo(({
setError()
}, [])
// The final step value. If an explicit step is given, it is used. Otherwise
// the available range is broken down into a number of steps, and the closest
// power of ten (in the current unit system) is used.
const rangeSI = maxLocal - minLocal
const rangeCustom = toUnitSystem(rangeSI, unitSI, units)
const stepFinalCustom = Math.pow(10, (Math.ceil(Math.log10(rangeCustom / nSteps))))
const stepFinalSI = toSI(stepFinalCustom, units[getDimension(unitSI)])
const stepFinal = stepSI || stepFinalSI || undefined
return <div className={clsx(className, styles.root)} data-testid={testID}>
<InputHeader
quantity={quantity}
......@@ -263,6 +276,7 @@ const InputSlider = React.memo(({
disabled={disabled}
label="min"
className={styles.textField}
inputProps={{className: styles.textInput}}
value={minText}
margin="normal"
onChange={handleChange(startChanged, setMinText)}
......@@ -275,7 +289,7 @@ const InputSlider = React.memo(({
color="secondary"
min={minLocal}
max={maxLocal}
step={stepSI}
step={stepFinal}
value={[range.gte, range.lte]}
onChange={handleRangeChange}
onChangeCommitted={handleRangeCommit}
......@@ -306,7 +320,8 @@ InputSlider.propTypes = {
label: PropTypes.string,
quantity: PropTypes.string.isRequired,
description: PropTypes.string,
step: PropTypes.oneOfType([PropTypes.number, PropTypes.object]).isRequired,
step: PropTypes.oneOfType([PropTypes.number, PropTypes.object]),
nSteps: PropTypes.number,
visible: PropTypes.bool,
className: PropTypes.string,
classes: PropTypes.object,
......@@ -314,4 +329,8 @@ InputSlider.propTypes = {
'data-testid': PropTypes.string
}
InputSlider.defaultProps = {
nSteps: 20
}
export default InputSlider
......@@ -20,7 +20,6 @@ import { makeStyles } from '@material-ui/core/styles'
import { Typography, Tooltip } from '@material-ui/core'
import PropTypes from 'prop-types'
import clsx from 'clsx'
import { startCase } from 'lodash'
import searchQuantities from '../../../searchQuantities'
import { useSearchContext } from '../SearchContext'
import { inputSectionContext } from './InputSection'
......@@ -42,8 +41,6 @@ const InputTitle = React.memo(({
quantity,
description,
variant,
underscores,
capitalize,
TooltipProps,
onMouseDown,
onMouseUp,
......@@ -59,20 +56,13 @@ const InputTitle = React.memo(({
// Remove underscores from name
const finalLabel = useMemo(() => {
let label = filterData[quantity]?.label
if (!label) {
label = searchQuantities[quantity]?.name || quantity
label = !underscores ? label.replace(/_/g, ' ') : label
if (capitalize) {
label = startCase(label)
}
}
const unit = filterData[quantity]?.unit
if (unit) {
const unitDef = new Unit(unit)
label = `${label} (${unitDef.label(units)})`
}
return label
}, [capitalize, filterData, quantity, underscores, units])
}, [filterData, quantity, units])
const finalDescription = description || filterData[quantity].description || searchQuantities[quantity]?.description
......@@ -94,18 +84,15 @@ InputTitle.propTypes = {
quantity: PropTypes.string.isRequired,
description: PropTypes.string,
variant: PropTypes.string,
underscores: PropTypes.bool,
className: PropTypes.string,
classes: PropTypes.object,
style: PropTypes.object,
capitalize: PropTypes.bool,
TooltipProps: PropTypes.object, // Properties forwarded to the Tooltip
onMouseDown: PropTypes.func,
onMouseUp: PropTypes.func
}
InputTitle.defaultProps = {
capitalize: true,
variant: 'body2'
}
......
......@@ -41,6 +41,7 @@ import FilterSubMenuAccess from './FilterSubMenuAccess'
import FilterSubMenuDataset from './FilterSubMenuDataset'
import FilterSubMenuIDs from './FilterSubMenuIDs'
import FilterSubMenuArchive from './FilterSubMenuArchive'
import FilterSubMenuWorkflow from './FilterSubMenuWorkflow'
import {
labelMaterial,
labelElements,
......@@ -60,7 +61,8 @@ import {
labelIDs,
labelAccess,
labelSpectroscopy,
labelArchive
labelArchive,
labelWorkflow
} from '../FilterRegistry'
import { useSearchContext } from '../SearchContext'
import InputCheckbox from '../input/InputCheckbox'
......@@ -115,6 +117,7 @@ const FilterMainMenu = React.memo(({
<FilterMenuItem value={labelVibrational} depth={1}/>
<FilterMenuItem value={labelMechanical} depth={1}/>
<FilterMenuItem value={labelSpectroscopy} depth={1}/>
<FilterMenuItem value={labelWorkflow} depth={1}/>
<FilterMenuItem value={labelAuthor} depth={0}/>
<FilterMenuItem value={labelDataset} depth={0}/>
<FilterMenuItem value={labelAccess} depth={0}/>
......@@ -145,6 +148,7 @@ const FilterMainMenu = React.memo(({
<FilterSubMenuVibrational value={labelVibrational}/>
<FilterSubMenuMechanical value={labelMechanical}/>
<FilterSubMenuSpectroscopy value={labelSpectroscopy}/>
<FilterSubMenuWorkflow value={labelWorkflow}/>
<FilterSubMenuAuthor value={labelAuthor}/>
<FilterSubMenuDataset value={labelDataset}/>
<FilterSubMenuAccess value={labelAccess}/>
......
......@@ -22,12 +22,9 @@ import { InputGrid, InputGridItem } from '../input/InputGrid'
import InputField from '../input/InputField'
import InputSlider from '../input/InputSlider'
import InputSection from '../input/InputSection'
import { Quantity, useUnits } from '../../../units'
import { useUnits } from '../../../units'
import { InputCheckboxValue } from '../input/InputCheckbox'
const stepResolution = new Quantity(0.1, 'electron_volt')
const stepEnergyWindow = new Quantity(10, 'electron_volt')
const FilterSubMenuEELS = React.memo(({
value,
...rest
......@@ -54,13 +51,11 @@ const FilterSubMenuEELS = React.memo(({
<InputSlider
quantity="results.properties.spectroscopy.eels.resolution"
units={units}
step={stepResolution}
visible={visible}
/>
<InputSlider
quantity="results.properties.spectroscopy.eels.energy_window"
units={units}
step={stepEnergyWindow}
visible={visible}
/>
<InputField
......
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext } from 'react'
import PropTypes from 'prop-types'
import { FilterSubMenu, filterMenuContext } from './FilterMenu'
import { InputGrid, InputGridItem } from '../input/InputGrid'
import InputSlider from '../input/InputSlider'
import InputSection from '../input/InputSection'
import InputField from '../input/InputField'
import { useUnits } from '../../../units'
const FilterSubMenuWorkflow = React.memo(({
value,
...rest
}) => {
const units = useUnits()
const {selected}