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", "name": "nomad-fair-gui",
"version": "0.9.11", "version": "0.10.0",
"commit": "e98694e", "commit": "e98694e",
"private": true, "private": true,
"workspaces": ["../dependencies/materia"],
"dependencies": { "dependencies": {
"@lauri-codes/materia": "0.0.10",
"@material-ui/core": "^4.0.0", "@material-ui/core": "^4.0.0",
"@material-ui/icons": "^4.0.0", "@material-ui/icons": "^4.0.0",
"@material-ui/lab": "^4.0.0-alpha.49", "@material-ui/lab": "^4.0.0-alpha.49",
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
"material-ui-chip-input": "^1.0.0-beta.14", "material-ui-chip-input": "^1.0.0-beta.14",
"material-ui-flat-pagination": "^4.0.0", "material-ui-flat-pagination": "^4.0.0",
"mathjs": "^7.1.0", "mathjs": "^7.1.0",
"nomad-fair-gui": "file:",
"object-hash": "^2.0.3", "object-hash": "^2.0.3",
"pace": "^0.0.4", "pace": "^0.0.4",
"pace-js": "^1.0.2", "pace-js": "^1.0.2",
...@@ -36,6 +37,7 @@ ...@@ -36,6 +37,7 @@
"react-autosuggest": "^9.4.3", "react-autosuggest": "^9.4.3",
"react-cookie": "^3.0.8", "react-cookie": "^3.0.8",
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.0.1",
"react-detectable-overflow": "^0.5.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-dropzone": "^5.0.1", "react-dropzone": "^5.0.1",
"react-highlight": "^0.12.0", "react-highlight": "^0.12.0",
......
...@@ -9,7 +9,7 @@ window.nomadEnv = { ...@@ -9,7 +9,7 @@ window.nomadEnv = {
'matomoUrl': 'https://nomad-lab.eu/fairdi/stat', 'matomoUrl': 'https://nomad-lab.eu/fairdi/stat',
'matomoSiteId': '2', 'matomoSiteId': '2',
'version': { 'version': {
'label': '0.9.11', 'label': '0.10.0',
'isBeta': false, 'isBeta': false,
'isTest': true, 'isTest': true,
'usesBetaData': true, 'usesBetaData': true,
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
*/ */
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import React from 'react' import React from 'react'
import { apiBase, appBase, optimadeBase } from '../config' import { apiBase, appBase } from '../config'
import Markdown from './Markdown' import Markdown from './Markdown'
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
...@@ -37,9 +37,20 @@ export default function About() { ...@@ -37,9 +37,20 @@ export default function About() {
# APIs # APIs
NOMAD's Application Programming Interface (API) allows you to access NOMAD data 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}/) - [API dashboard](${apiBase}/)
...@@ -54,7 +65,9 @@ export default function About() { ...@@ -54,7 +65,9 @@ export default function About() {
## OPTIMADE ## 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 [OPTIMADE](https://www.optimade.org/) is an
open API standard for materials science databases. This API can be used to search 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 = { ...@@ -122,13 +122,13 @@ ApiDialog.propTypes = {
onClose: PropTypes.func onClose: PropTypes.func
} }
export default function ApiDialogButton({component, ...dialogProps}) { export default function ApiDialogButton({component, size, ...dialogProps}) {
const [showDialog, setShowDialog] = useState(false) const [showDialog, setShowDialog] = useState(false)
return ( return (
<React.Fragment> <React.Fragment>
{component ? component({onClick: () => setShowDialog(true)}) : <Tooltip title="Show API code"> {component ? component({onClick: () => setShowDialog(true)}) : <Tooltip title="Show API code">
<IconButton onClick={() => setShowDialog(true)}> <IconButton size={size} onClick={() => setShowDialog(true)}>
<CodeIcon /> <CodeIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
...@@ -143,5 +143,6 @@ export default function ApiDialogButton({component, ...dialogProps}) { ...@@ -143,5 +143,6 @@ export default function ApiDialogButton({component, ...dialogProps}) {
ApiDialogButton.propTypes = { ApiDialogButton.propTypes = {
data: PropTypes.any.isRequired, data: PropTypes.any.isRequired,
title: PropTypes.string, title: PropTypes.string,
size: PropTypes.string,
component: PropTypes.func 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 @@ ...@@ -15,20 +15,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import React from 'react' import React, { useState } from 'react'
import clsx from 'clsx'
import { makeStyles } from '@material-ui/core/styles'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { import Alert from '@material-ui/lab/Alert'
Box, import { hasWebGLSupport } from '../utils'
Button,
Card,
CardContent,
Typography
} from '@material-ui/core'
import {
Error
} from '@material-ui/icons'
export class ErrorHandler extends React.Component { export class ErrorHandler extends React.Component {
state = { state = {
...@@ -48,88 +38,48 @@ export class ErrorHandler extends React.Component { ...@@ -48,88 +38,48 @@ export class ErrorHandler extends React.Component {
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
let msg = this.props.errorHandler ? this.props.errorHandler(this.state.error) : this.props.message let msg = typeof this.props.message === 'string' ? this.props.message : this.props.message(this.state.error)
return <ErrorCard return <Alert
message={msg} severity="error"
className={this.props.className} className={this.props.className}
classes={this.props.classes} classes={this.props.classes}
></ErrorCard> >
{msg}
</Alert>
} }
return this.props.children return this.props.children
} }
} }
ErrorHandler.propTypes = ({ ErrorHandler.propTypes = ({
children: PropTypes.object, children: PropTypes.object,
message: PropTypes.string, // Fixed error message. Provide either this or errorHandler message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), // Provide either a fixed error message or a callback that will receive the error details.
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.
classes: PropTypes.object, classes: PropTypes.object,
className: PropTypes.string className: PropTypes.string
}) })
export function ErrorCard({message, className, classes, actions}) { export const withErrorHandler = (WrappedComponent, message) => props => (
const useStyles = makeStyles((theme) => { <ErrorHandler message={message}>
return { <WrappedComponent {...props}></WrappedComponent>
root: { </ErrorHandler>)
color: theme.palette.error.main withErrorHandler.propTypes = ({
}, message: PropTypes.oneOfType([PropTypes.string, PropTypes.func]) // Provide either a fixed error message or a callback that will receive the error details.
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)
}
}
})
const style = useStyles(classes) export const withWebGLErrorHandler = WrappedComponent => props => {
console.log(actions) const hasWebGL = useState(hasWebGLSupport())[0]
return <Card className={clsx(style.root, className)}> // If WebGL is not available, the content cannot be shown.
<CardContent className={[style.content, style['content:last-child']].join(' ')}> if (hasWebGL) {
<Box className={style.row}> return WrappedComponent({...props})
<Error className={style.errorIcon}/> } else {
<Box className={style.column}> return <Alert
<Typography className={style.title} color="error" gutterBottom> severity="info"
{message} >
</Typography> Could not display the visualization as your browser does not support WebGL content.
{actions </Alert>
? <Box className={style.actions}> }
{actions.map((action) => <Button key={action.label} onClick={action.onClick}>
{action.label}
</Button>
)}
</Box>
: ''
}
</Box>
</Box>
</CardContent>
</Card>
} }
ErrorCard.propTypes = ({ withErrorHandler.propTypes = ({
message: PropTypes.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,
actions: PropTypes.array
}) })
...@@ -34,6 +34,7 @@ class Quantity extends React.Component { ...@@ -34,6 +34,7 @@ class Quantity extends React.Component {
noWrap: PropTypes.bool, noWrap: PropTypes.bool,
row: PropTypes.bool, row: PropTypes.bool,
column: PropTypes.bool, column: PropTypes.bool,
flex: PropTypes.bool,
data: PropTypes.object, data: PropTypes.object,
quantity: PropTypes.oneOfType([ quantity: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,
...@@ -41,7 +42,8 @@ class Quantity extends React.Component { ...@@ -41,7 +42,8 @@ class Quantity extends React.Component {
]), ]),
withClipboard: PropTypes.bool, withClipboard: PropTypes.bool,
ellipsisFront: PropTypes.bool, ellipsisFront: PropTypes.bool,
hideIfUnavailable: PropTypes.bool hideIfUnavailable: PropTypes.bool,
description: PropTypes.string
} }
static styles = theme => ({ static styles = theme => ({
...@@ -72,9 +74,10 @@ class Quantity extends React.Component { ...@@ -72,9 +74,10 @@ class Quantity extends React.Component {
}, },
row: { row: {
display: 'flex', display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row', flexDirection: 'row',
'& > :not(:first-child)': { '& > :not(:last-child)': {
marginLeft: theme.spacing(3) marginRight: theme.spacing(3)
} }
}, },
column: { column: {
...@@ -84,17 +87,30 @@ class Quantity extends React.Component { ...@@ -84,17 +87,30 @@ class Quantity extends React.Component {
marginTop: theme.spacing(1) marginTop: theme.spacing(1)
} }
}, },
flex: {
display: 'flex',
flexDirection: 'row',