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

Merge branch 'overview-alternative' into 'v0.10.0'

Overview

See merge request !255
parents 47281d27 21af1a25
Pipeline #93001 passed with stages
in 31 minutes and 5 seconds
......@@ -194,6 +194,9 @@
[submodule "dependencies/encyclopedia-gui"]
path = dependencies/encyclopedia-gui
url = https://gitlab.mpcdf.mpg.de/nomad-lab/encyclopedia-gui.git
[submodule "dependencies/materia"]
path = dependencies/materia
url = https://github.com/nomad-coe/materia
[submodule "dependencies/nomad-dos-fingerprints"]
path = dependencies/nomad-dos-fingerprints
url = https://gitlab.mpcdf.mpg.de/nomad-lab/nomad-dos-fingerprints.git
......
......@@ -87,6 +87,7 @@ WORKDIR /app
ENV PATH /app/node_modules/.bin:$PATH
COPY gui/package.json /app/package.json
COPY gui/yarn.lock /app/yarn.lock
COPY dependencies/materia /dependencies/materia
RUN yarn
COPY gui /app
COPY --from=build /install/gui/src/metainfo.json /app/src/metainfo.json
......
Subproject commit c234b1b184534bbe334acfcd59fc4e0618612814
Subproject commit df1175fab88a5216da20b75f81c7374d8df938df
Subproject commit d4a5d39b6c012807d71597a3e695295e51b4eedd
......@@ -3,8 +3,8 @@
"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",
......
/*
* 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, variant, size, justifyContent, className, classes}) {
const actionsStyles = makeStyles((theme) => ({
root: {
display: 'flex',
width: '100%',
justifyContent: justifyContent
},
iconButton: {
backgroundColor: 'white',
marginRight: theme.spacing(1)
}
}))
const styles = actionsStyles(classes)
const buttonList = actions.map((value, idx) => {
return <Tooltip key={idx} title={value.tooltip}>
{variant === 'icon'
? <IconButton
size={size}
className={styles.iconButton}
onClick={value.onClick}
disabled={value.disabled}
href={value.href}>
{value.content}
</IconButton>
: <Button
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,
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'
}
......@@ -48,7 +48,7 @@ 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
let msg = typeof this.props.message === 'string' ? this.props.message : this.props.message(this.state.error)
return <ErrorCard
message={msg}
className={this.props.className}
......@@ -60,8 +60,7 @@ export class ErrorHandler extends React.Component {
}
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
})
......@@ -102,7 +101,6 @@ export function ErrorCard({message, className, classes, actions}) {
})
const style = useStyles(classes)
console.log(actions)
return <Card className={clsx(style.root, className)}>
<CardContent className={[style.content, style['content:last-child']].join(' ')}>
......@@ -133,3 +131,11 @@ ErrorCard.propTypes = ({
className: PropTypes.string,
actions: PropTypes.array
})
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.
})
......@@ -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
......@@ -127,6 +143,9 @@ class Quantity extends React.Component {
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 +159,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}>
......
......@@ -25,10 +25,10 @@ import Browser, { Item, Content, Compartment, List, Adaptor } from './Browser'
import { resolveRef, rootSections } from './metainfo'
import { Title, metainfoAdaptorFactory, DefinitionLabel } from './MetainfoBrowser'
import { Matrix, Number } from './visualizations'
import Structure from '../visualization/Structure'
import { Structure } from '../visualization/Structure'
import BrillouinZone from '../visualization/BrillouinZone'
import BandStructure from '../visualization/BandStructure'
import { ErrorHandler, ErrorCard } from '../ErrorHandler'
import { ErrorHandler } from '../ErrorHandler'
import DOS from '../visualization/DOS'
import { StructureViewer, BrillouinZoneViewer } from '@lauri-codes/materia'
import Markdown from '../Markdown'
......@@ -378,7 +378,6 @@ QuantityValue.propTypes = ({
function Overview({section, def}) {
// States
const [mode, setMode] = useState('bs')
const [warningIgnored, setWarningIgnored] = useState(false)
// Styles
const useStyles = makeStyles(
......@@ -391,8 +390,6 @@ function Overview({section, def}) {
width: '20rem',
height: '40rem'
},
error: {
},
radio: {
display: 'flex',
justifyContent: 'center'
......@@ -416,19 +413,16 @@ function Overview({section, def}) {
let index = tmpIndex === -1 ? tmp : tmp.slice(0, tmpIndex)
let system
let positionsOnly = false
// Do not attempt to perform visualization if size is too big
// The section is incomplete, we leave the overview empty
if (!section.atom_species) {
// the section is incomplete, we leave the overview empty
return ''
return null
}
const nAtoms = section.atom_species.length
// Loading exact same system, no need to reload visualizer
if (sectionPath === visualizedSystem.sectionPath && index === visualizedSystem.index) {
// Loading same system with different positions
} else if (sectionPath === visualizedSystem.sectionPath && nAtoms === visualizedSystem.nAtoms) {
positionsOnly = true
system = {
positions: convertSI(section.atom_positions, 'meter', {length: 'angstrom'}, false)
}
......@@ -436,15 +430,6 @@ function Overview({section, def}) {
// the first time, check the system size and for large systems ask the user
// for permission.
} else {
const sizeLimit = 300
if (nAtoms >= sizeLimit && !warningIgnored) {
return <ErrorCard
message={`Visualization is by default disabled for systems with more than ${sizeLimit} atoms. Do you wish to enable visualization for this system with ${nAtoms} atoms?`}
className={style.error}
actions={[{label: 'Yes', onClick: e => setWarningIgnored(true)}]}
>
</ErrorCard>
}
system = {
'species': section.atom_species,
'cell': section.lattice_vectors ? convertSI(section.lattice_vectors, 'meter', {length: 'angstrom'}, false) : undefined,
......@@ -456,46 +441,29 @@ function Overview({section, def}) {
visualizedSystem.index = index
visualizedSystem.nAtoms = nAtoms
return <ErrorHandler
message='Could not load structure viewer.'
className={style.error}
>
<Structure
viewer={viewer}
system={system}
positionsOnly={positionsOnly}
></Structure>
</ErrorHandler>
return <Structure
viewer={viewer}
system={system}
positionsOnly={true}
></Structure>
// Band structure plot for section_k_band
} else if (def.name === 'KBand') {
return <>
{mode === 'bs'
? <Box>
<ErrorHandler
message="Could not load the band structure."
className={style.error}
>
<BandStructure
className={style.bands}
data={section}
aspectRatio={1}
unitsState={unitsState}
></BandStructure>
</ErrorHandler>
<BandStructure
className={style.bands}
data={section}
aspectRatio={1}
unitsState={unitsState}
></BandStructure>
</Box>
: <>
<ErrorHandler
message="Could not load the Brillouin zone."
className={style.error}
>
<BrillouinZone
viewer={bzViewer}
className={style.bands}
data={section}
aspectRatio={1}
></BrillouinZone>
</ErrorHandler>
</>
: <BrillouinZone
viewer={bzViewer}
className={style.bands}
data={section}
aspectRatio={1}
></BrillouinZone>
}
<FormControl component="fieldset" className={style.radio}>
<RadioGroup row aria-label="position" name="position" defaultValue="bs" onChange={toggleMode} className={style.radio}>
......@@ -516,17 +484,12 @@ function Overview({section, def}) {
</>
// DOS plot for section_dos
} else if (def.name === 'Dos') {
return <ErrorHandler
message="Could not load the density of states"
className={style.error}
>
<DOS
className={style.dos}
data={section}
aspectRatio={1 / 2}
unitsState={unitsState}
></DOS>
</ErrorHandler>
return <DOS
className={style.dos}
data={section}
aspectRatio={1 / 2}
unitsState={unitsState}
></DOS>
}
return null
}
......
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.