Commit 873efcae authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Merge branch 'v0.10.0' into 'master'

Merge v0.10.0 into master for release

Closes #475, #484, #497, #492, #498, and #500

See merge request !283
parents 5636e115 015742db
Pipeline #95934 passed with stage
in 2 minutes and 28 seconds
{
"name": "nomad-fair-gui",
"version": "0.9.11",
"version": "0.10.0",
"commit": "e98694e",
"private": true,
"workspaces": ["../dependencies/materia"],
"dependencies": {
"@lauri-codes/materia": "0.0.10",
"@material-ui/core": "^4.0.0",
"@material-ui/icons": "^4.0.0",
"@material-ui/lab": "^4.0.0-alpha.49",
......@@ -25,6 +25,7 @@
"material-ui-chip-input": "^1.0.0-beta.14",
"material-ui-flat-pagination": "^4.0.0",
"mathjs": "^7.1.0",
"nomad-fair-gui": "file:",
"object-hash": "^2.0.3",
"pace": "^0.0.4",
"pace-js": "^1.0.2",
......@@ -36,6 +37,7 @@
"react-autosuggest": "^9.4.3",
"react-cookie": "^3.0.8",
"react-copy-to-clipboard": "^5.0.1",
"react-detectable-overflow": "^0.5.0",
"react-dom": "^16.13.1",
"react-dropzone": "^5.0.1",
"react-highlight": "^0.12.0",
......
......@@ -9,7 +9,7 @@ window.nomadEnv = {
'matomoUrl': 'https://nomad-lab.eu/fairdi/stat',
'matomoSiteId': '2',
'version': {
'label': '0.9.11',
'label': '0.10.0',
'isBeta': false,
'isTest': true,
'usesBetaData': true,
......
......@@ -17,7 +17,7 @@
*/
import { makeStyles } from '@material-ui/core'
import React from 'react'
import { apiBase, appBase, optimadeBase } from '../config'
import { apiBase, appBase } from '../config'
import Markdown from './Markdown'
const useStyles = makeStyles(theme => ({
......@@ -37,9 +37,20 @@ export default function About() {
# APIs
NOMAD's Application Programming Interface (API) allows you to access NOMAD data
and functions programatically.
and functions programatically. For all APIs, we offer dashboards that let you use
each API interactively, right in your browser.
## NOMAD's main API
## NOMAD's new (Version 1) API
- [API dashboard](${apiBase}/v1/extensions/docs)
- [API documentation](${apiBase}/v1/extensions/redoc)
We started to implement a more consise and easier to use API for access NOMAD
data. This will step-by-step reimplement all functions of NOMAD's old main API.
At some point, it will replace it entirely. For new users, we recommend to start
using this API. API Dashboard and documentation contain a tutorial on how to get started.
## NOMAD's main (Version 0) API
- [API dashboard](${apiBase}/)
......@@ -54,7 +65,9 @@ export default function About() {
## OPTIMADE
- [OPTIMADE API dashboard](${optimadeBase}/)
- [OPTIMADE API overview page](${appBase}/optimade/)
- [OPTIMADE API dashboard](${appBase}/optimade/v1/extensions/docs)
- [OPTIMADE API documentation](${appBase}/optimade/v1/extensions/redoc)
[OPTIMADE](https://www.optimade.org/) is an
open API standard for materials science databases. This API can be used to search
......
/*
* 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 from 'react'
import PropTypes from 'prop-types'
import { Tooltip, IconButton, Button, Box, makeStyles } from '@material-ui/core'
import clsx from 'clsx'
export default function Actions({actions, color, variant, size, justifyContent, className, classes}) {
const actionsStyles = makeStyles((theme) => ({
root: {
display: 'flex',
width: '100%',
justifyContent: justifyContent
},
iconButton: {
marginRight: theme.spacing(1)
}
}))
const styles = actionsStyles(classes)
const buttonList = actions.map((value, idx) => {
return <Tooltip key={idx} title={value.tooltip}>
{variant === 'icon'
? <IconButton
color={color}
size={size}
className={styles.iconButton}
onClick={value.onClick}
disabled={value.disabled}
href={value.href}>
{value.content}
</IconButton>
: <Button
color={color}
variant={variant}
size={size}
className={styles.iconButton}
onClick={value.onClick}
disabled={value.disabled}
href={value.href}>
{value.content}
</Button>
}
</Tooltip>
})
return <Box className={clsx(className, styles.root)}>
{buttonList}
</Box>
}
Actions.propTypes = {
actions: PropTypes.array,
color: PropTypes.string,
variant: PropTypes.string,
size: PropTypes.string,
justifyContent: PropTypes.string,
className: PropTypes.string,
classes: PropTypes.string
}
Actions.defaultProps = {
size: 'small',
variant: 'icon',
justifyContent: 'flex-end'
}
......@@ -122,13 +122,13 @@ ApiDialog.propTypes = {
onClose: PropTypes.func
}
export default function ApiDialogButton({component, ...dialogProps}) {
export default function ApiDialogButton({component, size, ...dialogProps}) {
const [showDialog, setShowDialog] = useState(false)
return (
<React.Fragment>
{component ? component({onClick: () => setShowDialog(true)}) : <Tooltip title="Show API code">
<IconButton onClick={() => setShowDialog(true)}>
<IconButton size={size} onClick={() => setShowDialog(true)}>
<CodeIcon />
</IconButton>
</Tooltip>
......@@ -143,5 +143,6 @@ export default function ApiDialogButton({component, ...dialogProps}) {
ApiDialogButton.propTypes = {
data: PropTypes.any.isRequired,
title: PropTypes.string,
size: PropTypes.string,
component: PropTypes.func
}
/*
* 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, { useRef, useEffect } from 'react'
import PropTypes from 'prop-types'
import { IconButton, Card, CardActions, CardHeader, makeStyles } from '@material-ui/core'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import clsx from 'clsx'
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexDirection: 'column',
transition: theme.transitions.create('height', {
duration: theme.transitions.duration.shortest
})
},
cardHeader: {
flex: '0 0 1.5rem',
paddingBottom: theme.spacing(1.5),
height: '3rem'
},
cardContent: {
padding: theme.spacing(2),
paddingBottom: 0,
paddingTop: 0,
position: 'relative',
display: 'flex',
flexDirection: 'column',
flex: '0 1 auto',
overflow: 'hidden'
},
cardFixedContent: {
padding: theme.spacing(2),
paddingTop: 0,
paddingBottom: 0,
flex: '0 0 auto'
},
cardActions: {
flex: '0 0 1.5rem'
},
vspace: {
flex: '1 1 0'
},
expand: {
transform: 'rotate(0deg)',
marginLeft: 'auto',
transition: theme.transitions.create('transform', {
duration: theme.transitions.duration.shortest
})
},
limiter: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: '1rem',
background: 'linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 100%)'
},
expandOpen: {
transform: 'rotate(180deg)'
}
}))
/**
* Shows an informative overview about the selected entry.
*/
export default function CollapsibleCard({height, title, action, content, fixedContent}) {
const classes = useStyles()
const [expanded, setExpanded] = React.useState(false)
const [expandable, setExpandable] = React.useState(false)
const [expandedHeight, setExpandedHeight] = React.useState(false)
const root = useRef(null)
const collapsible = useRef(null)
const handleExpandClick = () => {
setExpanded(!expanded)
}
// Figure out the expanded size and whether the card is expandable once on
// startup
useEffect(() => {
setExpandedHeight(root.current.offsetHeight + collapsible.current.scrollHeight - collapsible.current.offsetHeight)
const hasOverflowingChildren = collapsible.current.offsetHeight < collapsible.current.scrollHeight ||
collapsible.current.offsetWidth < collapsible.current.scrollWidth
setExpandable(hasOverflowingChildren)
}, [])
return <Card ref={root} className={classes.root} style={{height: expanded ? expandedHeight : height}}>
<CardHeader
title={title}
className={classes.cardHeader}
action={action}
/>
<div ref={collapsible} className={classes.cardContent}>
<div style={{boxSizing: 'border-box'}}>
{content}
{ expandable && !expanded
? <div className={classes.limiter}></div>
: null
}
</div>
</div>
<div className={classes.vspace}></div>
{fixedContent
? <div className={classes.cardFixedContent}>
{fixedContent}
</div>
: null
}
<div className={classes.vspace}></div>
<CardActions
disableSpacing
className={classes.cardActions}
>
{expandable
? <IconButton
size='small'
className={clsx(classes.expand, {
[classes.expandOpen]: expanded
})}
disabled={!expandable}
onClick={handleExpandClick}
aria-expanded={expanded}
aria-label="show more"
>
<ExpandMoreIcon />
</IconButton>
: null
}
</CardActions>
</Card>
}
CollapsibleCard.propTypes = {
height: PropTypes.string,
title: PropTypes.string,
action: PropTypes.object,
content: PropTypes.object,
fixedContent: PropTypes.object
}
CollapsibleCard.defaultProps = {
height: '32rem'
}
......@@ -15,20 +15,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import clsx from 'clsx'
import { makeStyles } from '@material-ui/core/styles'
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import {
Box,
Button,
Card,
CardContent,
Typography
} from '@material-ui/core'
import {
Error
} from '@material-ui/icons'
import Alert from '@material-ui/lab/Alert'
import { hasWebGLSupport } from '../utils'
export class ErrorHandler extends React.Component {
state = {
......@@ -48,88 +38,48 @@ export class ErrorHandler extends React.Component {
render() {
if (this.state.hasError) {
let msg = this.props.errorHandler ? this.props.errorHandler(this.state.error) : this.props.message
return <ErrorCard
message={msg}
let msg = typeof this.props.message === 'string' ? this.props.message : this.props.message(this.state.error)
return <Alert
severity="error"
className={this.props.className}
classes={this.props.classes}
></ErrorCard>
>
{msg}
</Alert>
}
return this.props.children
}
}
ErrorHandler.propTypes = ({
children: PropTypes.object,
message: PropTypes.string, // Fixed error message. Provide either this or errorHandler
errorHandler: PropTypes.func, // Function that is called once an error is caught. It recveives the error object as argument and should return an error message as string.
message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), // Provide either a fixed error message or a callback that will receive the error details.
classes: PropTypes.object,
className: PropTypes.string
})
export function ErrorCard({message, className, classes, actions}) {
const useStyles = makeStyles((theme) => {
return {
root: {
color: theme.palette.error.main
},
content: {
paddingBottom: '16px'
},
'content:last-child': {
paddingBottom: '16px !important'
},
title: {
marginBottom: 0
},
pos: {
marginBottom: 12
},
row: {
display: 'flex'
},
actions: {
display: 'flex',
justifyContent: 'flex-end'
},
column: {
display: 'flex',
flexDirection: 'column'
},
errorIcon: {
marginRight: theme.spacing(1)
}
}
})
export const withErrorHandler = (WrappedComponent, message) => props => (
<ErrorHandler message={message}>
<WrappedComponent {...props}></WrappedComponent>
</ErrorHandler>)
withErrorHandler.propTypes = ({
message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) // Provide either a fixed error message or a callback that will receive the error details.
})
const style = useStyles(classes)
console.log(actions)
export const withWebGLErrorHandler = WrappedComponent => props => {
const hasWebGL = useState(hasWebGLSupport())[0]
return <Card className={clsx(style.root, className)}>
<CardContent className={[style.content, style['content:last-child']].join(' ')}>
<Box className={style.row}>
<Error className={style.errorIcon}/>
<Box className={style.column}>
<Typography className={style.title} color="error" gutterBottom>
{message}
</Typography>
{actions
? <Box className={style.actions}>
{actions.map((action) => <Button key={action.label} onClick={action.onClick}>
{action.label}
</Button>
)}
</Box>
: ''
// If WebGL is not available, the content cannot be shown.
if (hasWebGL) {
return WrappedComponent({...props})
} else {
return <Alert
severity="info"
>
Could not display the visualization as your browser does not support WebGL content.
</Alert>
}
</Box>
</Box>
</CardContent>
</Card>
}
ErrorCard.propTypes = ({
message: PropTypes.string,
classes: PropTypes.object,
className: PropTypes.string,
actions: PropTypes.array
withErrorHandler.propTypes = ({
message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) // Provide either a fixed error message or a callback that will receive the error details.
})
......@@ -34,6 +34,7 @@ class Quantity extends React.Component {
noWrap: PropTypes.bool,
row: PropTypes.bool,
column: PropTypes.bool,
flex: PropTypes.bool,
data: PropTypes.object,
quantity: PropTypes.oneOfType([
PropTypes.string,
......@@ -41,7 +42,8 @@ class Quantity extends React.Component {
]),
withClipboard: PropTypes.bool,
ellipsisFront: PropTypes.bool,
hideIfUnavailable: PropTypes.bool
hideIfUnavailable: PropTypes.bool,
description: PropTypes.string
}
static styles = theme => ({
......@@ -72,9 +74,10 @@ class Quantity extends React.Component {
},
row: {
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
'& > :not(:first-child)': {
marginLeft: theme.spacing(3)
'& > :not(:last-child)': {
marginRight: theme.spacing(3)
}
},
column: {
......@@ -84,17 +87,30 @@ class Quantity extends React.Component {
marginTop: theme.spacing(1)
}
},
flex: {
display: 'flex',
flexDirection: 'row',
flexWrap: 'wrap',
alignContent: 'flex-start',
'& div': {
marginRight: theme.spacing(1)
}
},
label: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: '0.75rem',
fontWeight: 500
},
quantityList: {
display: 'flex',
flexDirection: 'column'
}
})
render() {
const {
classes, children, label, typography, loading, placeholder, noWrap, row, column,
quantity, data, withClipboard, ellipsisFront, hideIfUnavailable
classes, children, label, typography, loading, placeholder, noWrap, row, column, flex,
quantity, data, withClipboard, ellipsisFront, hideIfUnavailable, description
} = this.props
let content = null
let clipboardContent = null
......@@ -120,13 +136,20 @@ class Quantity extends React.Component {
value = 'unavailable'
}
if ((!value || value === 'unavailable') && hideIfUnavailable) {
if (value === 'unavailable') {
value = ''
}
if (!value && hideIfUnavailable) {
return ''
}
if (children && children.length !== 0) {
content = children
} else if (value) {
if (Array.isArray(value)) {
value = value.join(', ')
}
clipboardContent = value
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
{value}
......@@ -140,11 +163,11 @@ class Quantity extends React.Component {
const useLabel = label || (typeof quantity === 'string' ? quantity : 'MISSING LABEL')
if (row || column) {
return <div className={row ? classes.row : classes.column}>{children}</div>
if (row || column || flex) {
return <div className={row ? classes.row : (column ? classes.column : classes.flex)}>{children}</div>
} else {
return (
<Tooltip title={(searchQuanitites[quantity] && searchQuanitites[quantity].description) || ''}>
<Tooltip title={description || (searchQuanitites[quantity] && searchQuanitites[quantity].description) || ''}>
<div className={classes.root}>
<Typography noWrap classes={{root: classes.label}} variant="caption">{useLabel}</Typography>
<div className={classes.valueContainer}>
......
......@@ -513,7 +513,7 @@ class Api {
this.onStartLoading()
return this.swagger()
.then(client => client.apis.repo.search({
exclude: ['atoms', 'only_atoms', 'dft.files', 'dft.quantities', 'dft.optimade', 'dft.labels', 'dft.geometries'],
exclude: ['atoms', 'only_atoms', 'files', 'dft.quantities', 'dft.optimade', 'dft.labels', 'dft.geometries'],
...search}))
.catch(handleApiError)
.then(response => response.body)
......
......@@ -28,13 +28,13 @@ import { Matrix, Number } from './visualizations'
import Structure from '../visualization/Structure'
import BrillouinZone from '../visualization/BrillouinZone'
import BandStructure from '../visualization/BandStructure'