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

Merge branch 'v1.0.2' into 'master'

Merge for release.

See merge request !556
parents 7595cc5f 2b5025fb
Pipeline #122334 failed with stages
in 20 minutes and 56 seconds
......@@ -205,19 +205,19 @@ deploy prod:
- /^dev-.*$/
when: manual
# deploy prod beta:
# stage: release
# before_script:
# - mkdir -p /etc/deploy
# - echo ${CI_K8S_PROD_CONFIG} | base64 -d > ${KUBECONFIG}
# script:
# - helm dependency update ops/helm/nomad
# - helm upgrade --install nomad-beta ops/helm/nomad -f ops/helm/nomad/deployments/prod-beta-values.yaml --set image.tag=$CI_COMMIT_REF_NAME,roll=true --wait
# - docker pull $TEST_IMAGE
# - docker run -t -e NOMAD_KEYCLOAK_REALM_NAME=fairdi_nomad_prod $TEST_IMAGE python -m nomad.cli client -n https://nomad-lab.eu/prod/rae/beta/api -u test -w $CI_NOMAD_TEST_PASSWORD integrationtests --skip-publish --skip-doi
# except:
# - /^dev-.*$/
# when: manual
deploy prod staging:
stage: release
before_script:
- mkdir -p /etc/deploy
- echo ${CI_K8S_PROD_CONFIG} | base64 -d > ${KUBECONFIG}
script:
- helm dependency update ops/helm/nomad
- helm upgrade --install nomad-staging-v1 ops/helm/nomad -f ops/helm/nomad/deployments/prod-staging-values.yaml --set image.tag=$CI_COMMIT_REF_NAME,roll=true --wait
- docker pull $TEST_IMAGE
- docker run -t -e NOMAD_KEYCLOAK_REALM_NAME=fairdi_nomad_prod $TEST_IMAGE python -m nomad.cli client -n https://nomad-lab.eu/prod/v1/staging/api -u test -w $CI_NOMAD_TEST_PASSWORD integrationtests --skip-publish --skip-doi
except:
- /^dev-.*$/
when: manual
deploy prod test:
stage: release
......
Subproject commit a8a92c6ebf6734cf0813d807c2309a9e5310ff3b
Subproject commit ef6874c976ab4f801f652254aeedfa8852bce0dd
......@@ -95,6 +95,20 @@ unix/linux systems. It can be installed on MacOS with homebrew:
brew install libmagic
```
### Install nomad
Finally, you can add nomad to the environment itself (including all extras).
The `-e` option will install the NOMAD with symbolic links allowing you
to change the code without having to reinstall after each change.
```sh
pip install -e .[all]
```
If pip tries to use and compile sources and this creates errors, it can be told to prefer binary version:
```sh
pip install -e .[all] --prefer-binary
```
### Install sub-modules
Nomad is based on python modules from the NOMAD-coe project.
This includes parsers, python-common and the meta-info. These modules are maintained as
......@@ -118,18 +132,6 @@ to install set packages manually.
The `-e` option will install the NOMAD-coe dependencies with symbolic links allowing you
to change the downloaded dependency code without having to reinstall after.
### Install nomad
Finally, you can add nomad to the environment itself (including all extras)
```sh
pip install -e .[all]
```
If pip tries to use and compile sources and this creates errors, it can be told to prefer binary version:
```sh
pip install -e .[all] --prefer-binary
```
### Generate GUI artifacts
The NOMAD GUI requires static artifacts that are generated from the NOMAD Python codes.
```sh
......
{
"name": "nomad-fair-gui",
"version": "1.0.1",
"version": "1.0.2",
"commit": "e98694e",
"private": true,
"workspaces": [
......
......@@ -11,7 +11,7 @@ window.nomadEnv = {
'encyclopediaBase': 'https://nomad-lab.eu/prod/rae/encyclopedia/#',
'debug': false,
'version': {
'label': '1.0.1',
'label': '1.0.2',
'isBeta': false,
'isTest': true,
'usesBetaData': true,
......
......@@ -242,13 +242,13 @@ export default function About() {
<Markdown>{`
# **NOMAD** &ndash; Manage and Publish Materials Data
This is the *graphical user interface* (GUI) for the NOMAD Repository and
Archive. It allows you to **search, access, and download all NOMAD data** in its
raw (Repository) and processed (Archive) form. You can **upload and manage your own
This is the *graphical user interface* (GUI) of NOMAD. It allows you to **search,
access, and download all NOMAD data** in its
*raw files* and *processed data* form. You can **upload and manage your own
raw materials science data**. You can access all published data without an account.
If you want to provide your own data, please login or register for an account.
You can learn more about on the NOMAD Repository and Archive
You can learn more about NOMAD on its
[homepage](https://nomad-lab.eu/repo-arch), our
[documentation](${appBase}/docs/index.html).
There is also an [FAQ](https://nomad-lab.eu/repository-archive-faqs)
......@@ -263,8 +263,8 @@ export default function About() {
the <b>Optimade</b> filter language to add arbitrarily nested queries.
</InfoCard>
<InfoCard xs={6} title="A common data format" top>
The <b>NOMAD Archive</b> provides data in processed and normalized form in a machine processable and common hierarchical format.
All data in the NOMAD Archive is organized into nested sections of quantities with well defined units,
NOMAD provides data in <i>processed</i> and <i>normalized</i> form in a machine processable and common hierarchical format.
This <i>processed data</i>, i.e. the <b>NOMAD Archive</b>, is organized into nested sections of quantities with well defined units,
data types, shapes, and descriptions. These definitions are called the <b>NOMAD Metainfo</b> and they
can be <Link component={RouterLink} to={'/metainfo'}>browsed here</Link>.
</InfoCard>
......@@ -296,9 +296,9 @@ export default function About() {
<InfoCard xs={4} title="Processing" bottom>
<p>
Uploaded data is automatically processed and made available
in the uploaded <b>raw files</b> or in its processed and unified <b>Archive</b> form.
in the uploaded <b>raw files</b> or in its unified <b>processed data</b> form.
NOMAD parsers convert raw files into NOMAD&apos;s common data format.
You can inspect the Archive form and extracted metadata before
You can inspect the processed data and extracted metadata before
publishing your data.
</p>
<p>NOMAD supports most community codes and file formats: <CodeList/></p>
......@@ -338,7 +338,7 @@ export default function About() {
Oasis), how to contribute parsers, and much more.
### Source code
The source-code for the NOMAD Repository and Archive is maintained
The source-code for NOMAD is maintained
at the MPCDF's [gitlab](https://gitlab.mpcdf.mpg.de/nomad-lab/nomad-FAIR).
To push code, you need an MPCDF account and you can apply
[here](https://www.mpcdf.mpg.de/userspace/forms/onlineregistrationform).
......
......@@ -30,7 +30,9 @@ const useStyles = makeStyles(theme => ({
},
limitedWidth: {
width: '100%',
maxWidth: limitedWidthWidth,
maxWidth: limitedWidthWidth + theme.spacing(2),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
marginLeft: 'auto',
marginRight: 'auto'
},
......
......@@ -54,11 +54,10 @@ export default function ArchiveBrowser({data}) {
// the Browser (notably: Quantity, QuantityItemPreview). In order to pass the
// up-to-date unit information, we pass the hook value down the component
// hierarchy.
const units = useUnits()
data.resources = data.resources || {}
return (
<Browser
adaptor={archiveAdaptorFactory(data, undefined, units)}
adaptor={archiveAdaptorFactory(data, undefined)}
form={<ArchiveConfigForm searchOptions={searchOptions} data={data}/>}
/>
)
......@@ -160,8 +159,8 @@ ArchiveConfigForm.propTypes = ({
searchOptions: PropTypes.arrayOf(PropTypes.object).isRequired
})
function archiveAdaptorFactory(data, sectionDef, units) {
return new SectionAdaptor(data, sectionDef || rootSections.find(def => def.name === 'EntryArchive'), undefined, {archive: data}, units)
function archiveAdaptorFactory(data, sectionDef) {
return new SectionAdaptor(data, sectionDef || rootSections.find(def => def.name === 'EntryArchive'), undefined, {archive: data})
}
function archiveSearchOptions(data) {
......@@ -212,22 +211,21 @@ function archiveSearchOptions(data) {
}
class ArchiveAdaptor extends Adaptor {
constructor(obj, def, parent, context, units) {
constructor(obj, def, parent, context) {
super(obj)
this.def = def
this.parent = parent
this.context = context
this.units = units
}
adaptorFactory(obj, def, parent, context) {
if (def.m_def === 'Section') {
return new SectionAdaptor(obj, def, parent, context || this.context, this.units)
return new SectionAdaptor(obj, def, parent, context || this.context)
} else if (def.m_def === 'Quantity') {
if (def.type.type_kind === 'reference') {
return new ReferenceAdaptor(obj, def, parent, context || this.context, this.units)
return new ReferenceAdaptor(obj, def, parent, context || this.context)
} else {
return new QuantityAdaptor(obj, def, parent, context || this.context, this.units)
return new QuantityAdaptor(obj, def, parent, context || this.context)
}
}
}
......@@ -285,23 +283,24 @@ class SectionAdaptor extends ArchiveAdaptor {
}
}
render() {
return <Section section={this.e} def={this.def} parent={this.parent} units={this.units}/>
return <Section section={this.e} def={this.def} parent={this.parent} />
}
}
class ReferenceAdaptor extends ArchiveAdaptor {
render() {
return <Reference value={this.e} def={this.def} units={this.units}/>
return <Reference value={this.e} def={this.def} />
}
}
class QuantityAdaptor extends ArchiveAdaptor {
render() {
return <Quantity value={this.e} def={this.def} units={this.units}/>
return <Quantity value={this.e} def={this.def} />
}
}
function QuantityItemPreview({value, def, units}) {
function QuantityItemPreview({value, def}) {
const units = useUnits()
if (def.type.type_kind === 'reference') {
return <Box component="span" fontStyle="italic">
<Typography component="span">reference ...</Typography>
......@@ -348,11 +347,11 @@ function QuantityItemPreview({value, def, units}) {
}
QuantityItemPreview.propTypes = ({
value: PropTypes.any,
def: PropTypes.object.isRequired,
units: PropTypes.object
def: PropTypes.object.isRequired
})
function QuantityValue({value, def, units}) {
function QuantityValue({value, def}) {
const units = useUnits()
const val = (def.type.type_data === 'nomad.metainfo.metainfo._Datetime' ? new Date(value).toLocaleString() : value)
const [finalValue, finalUnit] = def.unit
? toUnitSystem(val, def.unit, units, true)
......@@ -392,11 +391,10 @@ function QuantityValue({value, def, units}) {
}
QuantityValue.propTypes = ({
value: PropTypes.any,
def: PropTypes.object.isRequired,
units: PropTypes.object
def: PropTypes.object.isRequired
})
function Section({section, def, parent, units}) {
function Section({section, def, parent}) {
const config = useRecoilValue(configState)
if (!section) {
......@@ -414,7 +412,7 @@ function Section({section, def, parent, units}) {
const quantities = def._allProperties.filter(prop => prop.m_def === 'Quantity')
return <Content>
<Title def={def} data={section} kindLabel="section" />
<Overview section={section} def={def} units={units}/>
<Overview section={section} def={def} />
<Compartment title="sub sections">
{sub_sections
.filter(subSectionDef => section[subSectionDef.name] || config.showAllDefined)
......@@ -462,7 +460,6 @@ function Section({section, def, parent, units}) {
<QuantityItemPreview
value={section[quantityDef.name]}
def={quantityDef}
units={units}
/>
</span>
}
......@@ -478,8 +475,7 @@ function Section({section, def, parent, units}) {
Section.propTypes = ({
section: PropTypes.object.isRequired,
def: PropTypes.object.isRequired,
parent: PropTypes.any,
units: PropTypes.object
parent: PropTypes.any
})
function SubSectionList({subSectionDef}) {
......@@ -571,14 +567,13 @@ PropertyValuesList.propTypes = ({
values: PropTypes.arrayOf(PropTypes.string).isRequired
})
function Quantity({value, def, units}) {
function Quantity({value, def}) {
return <Content>
<Title def={def} data={value} kindLabel="value" />
<Compartment title="value">
<QuantityValue
value={value}
def={def}
units={units}
/>
</Compartment>
<Meta def={def} />
......@@ -586,11 +581,10 @@ function Quantity({value, def, units}) {
}
Quantity.propTypes = ({
value: PropTypes.any,
def: PropTypes.object.isRequired,
units: PropTypes.object
def: PropTypes.object.isRequired
})
function Reference({value, def, units}) {
function Reference({value, def}) {
const {api} = useApi()
const {raiseError} = useErrors()
const [loading, setLoading] = useState(true)
......@@ -633,8 +627,7 @@ function Reference({value, def, units}) {
}
Reference.propTypes = ({
value: PropTypes.any,
def: PropTypes.object.isRequired,
units: PropTypes.object
def: PropTypes.object.isRequired
})
const useMetaStyles = makeStyles(theme => ({
......
......@@ -18,7 +18,6 @@
import React, { useContext, useRef, useLayoutEffect, useMemo, useState } from 'react'
import PropTypes from 'prop-types'
import { RecoilRoot } from 'recoil'
import { makeStyles, Card, CardContent, Box, Typography } from '@material-ui/core'
import grey from '@material-ui/core/colors/grey'
import ArrowRightIcon from '@material-ui/icons/ArrowRight'
......@@ -126,7 +125,7 @@ export const Browser = React.memo(function Browser({adaptor, form}) {
return lanes
}, [root, archivePath, memoedAdapters, render, setRender])
return <RecoilRoot>
return <React.Fragment>
{form}
<Card>
<CardContent>
......@@ -141,7 +140,7 @@ export const Browser = React.memo(function Browser({adaptor, form}) {
</div>
</CardContent>
</Card>
</RecoilRoot>
</React.Fragment>
})
Browser.propTypes = ({
adaptor: PropTypes.object.isRequired,
......
......@@ -12,7 +12,7 @@ import BrillouinZone from '../visualization/BrillouinZone'
import BandStructure from '../visualization/BandStructure'
import EELS from '../visualization/EELS'
import DOS from '../visualization/DOS'
import { toUnitSystem } from '../../units'
import { toUnitSystem, useUnits } from '../../units'
import { electronicRange } from '../../config'
import EnergyVolumeCurve from '../visualization/EnergyVolumeCurve'
......@@ -257,27 +257,27 @@ OverviewEquationOfState.propTypes = ({
export const Overview = React.memo((props) => {
const {def} = props
const units = useUnits()
let path = window.location.href.split('/').pop().split(':')[0]
if (def.name === 'BandStructure' && path === 'band_structure_electronic') {
return <OverviewBandstructureElectronic {...props}/>
return <OverviewBandstructureElectronic {...props} units={units}/>
} else if (def.name === 'BandStructure' && path === 'band_structure_phonon') {
return <OverviewBandstructurePhonon {...props}/>
return <OverviewBandstructurePhonon {...props} units={units}/>
} else if (def.name === 'Atoms' && path === 'atoms') {
return <OverviewAtoms {...props}/>
return <OverviewAtoms {...props} units={units} />
} else if (def.name === 'Dos' && path === 'dos_electronic') {
return <OverviewDOSElectronic {...props}/>
return <OverviewDOSElectronic {...props} units={units} />
} else if (def.name === 'Dos' && path === 'dos_phonon') {
return <OverviewDOSPhonon {...props}/>
return <OverviewDOSPhonon {...props} units={units}/>
} else if (def.name === 'Spectrum') {
return <OverviewEELS {...props}/>
return <OverviewEELS {...props} units={units}/>
} else if (def.name === 'EquationOfState') {
return <OverviewEquationOfState {...props}/>
return <OverviewEquationOfState {...props} units={units}/>
}
return null
})
Overview.propTypes = ({
def: PropTypes.object,
section: PropTypes.object,
units: PropTypes.object
section: PropTypes.object
})
......@@ -23,7 +23,8 @@ import { Button } from '@material-ui/core'
* Button that has a loading status and cannot be used while loading.
*/
export default function LoadingButton({loading, children, ...buttonProps}) {
return <Button disabled={loading} {...buttonProps}>
buttonProps.disabled = buttonProps.disabled || loading
return <Button {...buttonProps}>
{children}
</Button>
}
......
......@@ -42,12 +42,24 @@ very similar to labels, albums, or tags on other platforms.
`
const columns = [
{key: 'dataset_name'},
{
key: 'doi',
label: 'Digital object identifier (DOI)',
render: dataset => {
if (dataset.doi) {
return <Quantity
quantity={'datasets.doi'} noLabel noWrap withClipboard
data={{datasets: dataset}}
/>
}
return ''
}
},
{
key: 'dataset_id',
render: dataset => <Quantity quantity={'datasets.dataset_id'} noLabel noWrap withClipboard data={{datasets: dataset}}/>
},
{key: 'dataset_name'},
{key: 'doi', label: 'Digital object identifier (DOI)'},
{
key: 'dataset_create_time',
label: 'Create time',
......
......@@ -255,6 +255,14 @@ const DatatableHeader = React.memo(function DatatableHeader({actions}) {
}
}
const sortableColumns = useMemo(() => {
if (sortingColumns) {
return sortingColumns
} else {
return columns.filter(column => column.sortable).map(column => column.key)
}
}, [sortingColumns, columns])
return <TableHead>
<TableRow>
{withSelectionFeature && <TableCell padding="checkbox" classes={{stickyHeader: classes.stickyHeader}}>
......@@ -271,7 +279,7 @@ const DatatableHeader = React.memo(function DatatableHeader({actions}) {
align={column.align || 'right'}
sortDirection={order_by === column.key ? order : false}
>
{withSorting && sortingColumns?.includes(column.key) ? <TableSortLabel
{withSorting && sortableColumns.includes(column.key) ? <TableSortLabel
active={order_by === column.key}
direction={order_by === column.key ? order : 'asc'}
onClick={createSortHandler(column)}
......
......@@ -105,7 +105,7 @@ export default function ArchiveEntryView(props) {
</div> : <div>{
data
? <div>
<Typography>Archive data is not valid JSON. Displaying plain text instead.</Typography>
<Typography>Processed data is not valid JSON. Displaying plain text instead.</Typography>
<Card>
<CardContent>
<pre>{data || ''}</pre>
......
......@@ -173,7 +173,7 @@ export const VisitEntryAction = React.memo(function VisitEntryAction({data, ...p
// navigation that otherwise leaves the popup opened (the Tooltip state does
// not get updated since the page is cached and a new page is shown
// immediately).
return <Tooltip PopperProps={{disablePortal: true}} title="Show raw files and archive">
return <Tooltip PopperProps={{disablePortal: true}} title="Go to the entry page">
<EntryButton
{...props}
entryId={data.entry_id}
......@@ -246,7 +246,7 @@ export const EntryDetails = React.memo(({data}) => {
<div className={classes.entryDetailsActions}>
<VisitEntryAction color="primary" data={data}>
Show raw files and archive
Go to the entry page
</VisitEntryAction>
</div>
</div>
......
......@@ -85,7 +85,7 @@ const EntryDownloadButton = React.memo(function EntryDownloadButton(props) {
onClose={handleClose}
>
<MenuItem onClick={() => handleSelect('raw')}>Raw uploaded files</MenuItem>
<MenuItem onClick={() => handleSelect('archive')}>NOMAD Archive files</MenuItem>
<MenuItem onClick={() => handleSelect('archive')}>Processed data</MenuItem>
</Menu>
</React.Fragment>
})
......
......@@ -68,6 +68,9 @@ class ErrorSnacksUnstyled extends React.Component {
errorStr = 'There is a new GUI version available. Please press "shift" and reload the page.'
} else if (error.message.startsWith('could not parse optimade')) {
errorStr = 'The given OPTiMaDe query can not be parsed.'
} else if (error.status === 422) {
errorStr = `Unexpected api error: ${error.apiMessage[0].msg}. Please try again and let us know, if this error keeps happening.`
console.log(error)
} else if (error.message) {
errorStr = `Unexpected error: "${error.message}". Please try again and let us know, if this error keeps happening.`
console.log(error)
......
......@@ -91,7 +91,7 @@ const entryRoutes = [
{
path: 'archive',
exact: true,
breadcrumb: 'Archive'
breadcrumb: 'Processed data'
},
{
path: 'logs',
......@@ -234,7 +234,7 @@ export const routes = [
exact: true,
cache: 'always',
component: SearchPageEntries,
menu: 'Entries Repository',
menu: 'Entries',
tooltip: 'Search individual database entries',
breadcrumb: 'Entries search',
help: {
......@@ -310,7 +310,7 @@ export const routes = [
menu: 'Information',
component: About,
breadcrumb: 'About NOMAD',
tooltip: 'Overview of the NOMAD Repository and Archive'
tooltip: 'Overview of the NOMAD'
},
{
menu: 'Forum',
......
......@@ -15,7 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { isNil } from 'lodash'
import { isNil, isArray } from 'lodash'
import { setToArray, getDatatype, getSerializer, getDeserializer, getLabel } from '../../utils'
import searchQuantities from '../../searchQuantities'
import { getDimension, Quantity } from '../../units'
......@@ -49,7 +49,27 @@ export const labelAuthor = 'Author / Origin'
export const labelAccess = 'Access'
export const labelDataset = 'Dataset'
export const labelIDs = 'IDs'
export const labelArchive = 'Archive'
export const labelArchive = 'Processed data quantities'
/**
* Used to gather a list of fixed filter options from the metainfo.
* @param {string} quantity Metainfo name
* @returns Dictionary containing the available options and their labels.
*/
function getEnumOptions(quantity) {
const metainfoOptions = searchQuantities?.[quantity]?.type?.type_data
if (isArray(metainfoOptions) && metainfoOptions.length > 0) {
const opt = {}
for (const name of metainfoOptions) {
opt[name] = {label: name}
}
// We do not display the option for 'not processed': it is more of a
// debug value
delete opt['not processed']
return opt
}
}
/**
* This function is used to register a new filter within the SearchContext.
......@@ -118,21 +138,34 @@ function saveFilter(name, group, config) {
}
const data = filterData[name] || {}
data.options = config.options || getEnumOptions(name)
const agg = config.agg
if (agg) {
// Notice how here we have to introduce another inner function in order to
// get the value of "name" and "type" at the time this function is created.
data.aggGet = agg.get || ((name, type) => (aggs) => aggs[name][type].data)(name, agg)
data.aggSet = agg.set || {[name]: {[agg]: {}}}