Commit 02a33055 authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Initial version of new metainfo browser gui integration.

parent 27484fae
Pipeline #47251 failed with stages
in 30 minutes and 22 seconds
......@@ -242,13 +242,12 @@ export default class MetaInfoRepository {
return definition
}
const metaInfos = json || []
const metaInfos = json.metaInfos || []
const pkg = {
mType: schema.pkg,
name: name,
definitions: metaInfos
.filter(metaInfo => !metaInfo.name.startsWith('x_'))
.map(transformMetaInfo)
description: json.description,
definitions: metaInfos.map(transformMetaInfo)
}
this.addName(pkg)
......
......@@ -262,23 +262,28 @@ class Api {
.finally(this.onFinishLoading)
}
_metaInfoRepository = null
_metaInfoRepositories = {}
async getMetaInfo() {
if (this._metaInfoRepository) {
return this._metaInfoRepository
async getMetaInfo(pkg) {
pkg = pkg || 'all.nomadmetainfo.json'
const metaInfoRepository = this._metaInfoRepositories[pkg]
if (metaInfoRepository) {
return metaInfoRepository
} else {
this.onStartLoading()
const loadMetaInfo = async(path) => {
const client = await this.swaggerPromise
return client.apis.archive.get_metainfo({metainfo_path: path})
return client.apis.archive.get_metainfo({metainfo_package_name: path})
.catch(this.handleApiError)
.then(response => response.body)
}
const metaInfos = await loadMetaInfo('all.nomadmetainfo.json')
this._metaInfoRepository = new MetaInfoRepository({'all.nomadmetainfo.json': metaInfos})
const metaInfo = await loadMetaInfo(pkg)
const metaInfoRepository = new MetaInfoRepository(metaInfo)
this._metaInfoRepositories[pkg] = metaInfoRepository
this.onFinishLoading()
return this._metaInfoRepository
return metaInfoRepository
}
}
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import { Paper, Typography, Link } from '@material-ui/core'
import { Paper, Typography } from '@material-ui/core'
import { Link } from 'react-router-dom'
import { schema } from '../MetaInfoRepository'
import ReactJson from 'react-json-view'
import { Card, CardButton, CardCompartment, PopoverCardButton } from './util/cards'
......@@ -93,8 +94,9 @@ class DefinitionCardUnstyled extends React.Component {
onClick={() => toggleDefinition(definition.parent, !parentIsVisible)}/>
: ''
}
<CardButton position="center" size="tiny" icon="launch" component={Link}
to={`/metainfo/${definition.name}`} />
<CardButton position="center" size="tiny" icon="launch"
component={props => <Link to={`/metainfo/${definition.name}`} {...props} />}
/>
<PopoverCardButton position="center" icon="code" classes={{content: classes.source}} size="tiny">
<ReactJson src={definition.miJson} />
</PopoverCardButton>
......@@ -123,20 +125,9 @@ class DefinitionCardUnstyled extends React.Component {
}
renderDescription(description) {
// const {classes} = this.props
description = description.replace(/(([A-Za-z0-9]+_)+[A-Za-z0-9]+)/g, '`$1`')
// const paragraph = (props) => (
// <Typography classes={{root: classes.descriptionParagraph}}>{props.children}</Typography>
// )
// // apply mono space to definition names and add invisible zero space to each _ to allow line breaks
// const inlineCode = (props) => (
// <a href={`/${props.children}`} style={{fontFamily: 'RobotoMono, monospace', fontWeight: 'bold'}}>
// {props.children.replace(/_/g, `\u200B_`)}
// </a>
// )
return (
<Markdown>{description}</Markdown>
// <ReactMarkdown source={description} className={classes.description} renderers={{paragraph: paragraph, inlineCode: inlineCode}}/>
)
}
}
......
import React, { Component } from 'react'
import { withRouter } from 'react-router-dom'
import Viewer from './Viewer'
import PropTypes from 'prop-types'
import { withApi } from '../api'
import { Help } from '../help'
import MetainfoSearch from './MetainfoSearch'
import { FormControl, withStyles, Select, Input, MenuItem, ListItemText, InputLabel, FormGroup } from '@material-ui/core'
import { compose } from 'recompose'
const ITEM_HEIGHT = 48
const ITEM_PADDING_TOP = 8
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 300
}
}
}
class MetaInfoBrowser extends Component {
static propTypes = {
classes: PropTypes.object.isRequired,
metainfo: PropTypes.string,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired
loading: PropTypes.number,
raiseError: PropTypes.func.isRequired,
history: PropTypes.object.isRequired
}
static styles = theme => ({
root: {},
forms: {
padding: `${theme.spacing.unit * 3}px ${theme.spacing.unit * 3}px 0 ${theme.spacing.unit * 3}px`
},
packageSelect: {
width: 300
},
search: {
marginLeft: theme.spacing.unit * 3
}
})
state = {
metainfos: null
metainfos: null,
allMetainfos: null,
selectedPackage: null,
loadedPackage: null
}
componentDidMount() {
this.props.api.getMetaInfo().then(metainfos => {
this.setState({metainfos: metainfos})
constructor(props) {
super(props)
this.handleSelectedPackageChanged = this.handleSelectedPackageChanged.bind(this)
this.handleSearch = this.handleSearch.bind(this)
}
update(pkg) {
this.props.api.getMetaInfo(pkg).then(metainfos => {
const metainfoName = this.props.metainfo || 'section_run'
const definition = metainfos.get(metainfoName)
if (!definition) {
this.props.history.push('/metainfo/section_run')
} else {
this.setState({loadedPackage: pkg, metainfos: metainfos})
}
}).catch(error => {
this.props.raiseError(error)
})
}
init() {
this.props.api.getMetaInfo('all.nomadmetainfo.json').then(metainfos => {
const metainfoName = this.props.metainfo || 'section_run'
const definition = metainfos.get(metainfoName)
this.setState({allMetainfos: metainfos, selectedPackage: definition.package.name})
this.update(definition.package.name)
}).catch(error => {
this.props.raiseError(error)
})
}
componentDidUpdate(prevProps) {
if (this.props.metainfo !== prevProps.metainfo) {
this.init()
}
}
componentDidMount() {
this.init()
}
handleSelectedPackageChanged(event) {
this.setState({selectedPackage: event.target.value})
this.update(event.target.value)
}
handleSearch(term) {
if (this.state.metainfos.get(term)) {
this.props.history.push(`/metainfo/${term}`)
}
}
render() {
const { metainfos } = this.state
const { classes, loading } = this.props
const { metainfos, selectedPackage, allMetainfos, loadedPackage } = this.state
if (!metainfos) {
if (!metainfos || !allMetainfos) {
return <div />
}
const metainfoName = this.props.metainfo || 'section_run'
const metainfo = metainfos.resolve(metainfos.createProxy(metainfoName))
return <Viewer rootElement={metainfo}
packages={metainfos.contents}
visiblePackages={metainfos.contents.map(pkg => pkg.name)} />
return <div>
<div className={classes.forms}>
<Help cookie="uploadList">{`
The nomad *metainfo* is the schema that nomad uses to define the quantities that
comprise all nomad data. The nomad metainfo is consist of definitions for:
- **sections**: A section are nested groups of quantities that allow a hierarchical data structure
- **values**: Actual quantities that contain data
- **references**: References that allow to connect related sections.
All definitions have a name that you can search for. Furthermore, all definitions
are organized in packages. There is a *common* package with definitions that are
used by all codes and there are packages for each code with code specific definitions.
`}</Help>
<form style={{ display: 'flex' }}>
<FormControl disabled={loading > 0}>
<InputLabel htmlFor="select-multiple-checkbox">Package</InputLabel>
<Select
className={classes.packageSelect}
value={selectedPackage}
onChange={this.handleSelectedPackageChanged}
input={<Input id="select-multiple-checkbox" />}
MenuProps={MenuProps}
>
{allMetainfos.contents
.map(pkg => pkg.name)
.map(name => {
return <MenuItem key={name} value={name}>
<ListItemText primary={name.substring(0, name.length - 19)} />
</MenuItem>
})
}
</Select>
</FormControl>
<MetainfoSearch classes={{root: classes.search}}
suggestions={Object.values(metainfos.names)}
onChange={this.handleSearch}
/>
</form>
</div>
<Viewer key={loadedPackage} rootElement={metainfo} packages={metainfos.contents} />
</div>
}
}
export default withApi(false)(MetaInfoBrowser)
export default compose(withRouter, withApi(false), withStyles(MetaInfoBrowser.styles))(MetaInfoBrowser)
import React from 'react'
import PropTypes from 'prop-types'
import deburr from 'lodash/deburr'
import Autosuggest from 'react-autosuggest'
import match from 'autosuggest-highlight/match'
import parse from 'autosuggest-highlight/parse'
import TextField from '@material-ui/core/TextField'
import Paper from '@material-ui/core/Paper'
import MenuItem from '@material-ui/core/MenuItem'
import { withStyles } from '@material-ui/core/styles'
function renderInputComponent(inputProps) {
const { classes, inputRef = () => {}, ref, ...other } = inputProps
return (
<TextField
fullWidth
InputProps={{
inputRef: node => {
ref(node)
inputRef(node)
},
classes: {
input: classes.input
}
}}
{...other}
/>
)
}
function renderSuggestion(suggestion, { query, isHighlighted }) {
const matches = match(suggestion.name, query)
const parts = parse(suggestion.name, matches)
return (
<MenuItem selected={isHighlighted} component="div">
<div>
{parts.map((part, index) =>
part.highlight ? (
<span key={String(index)} style={{ fontWeight: 500 }}>
{part.text}
</span>
) : (
<strong key={String(index)} style={{ fontWeight: 300 }}>
{part.text}
</strong>
)
)}
</div>
</MenuItem>
)
}
function getSuggestionValue(suggestion) {
return suggestion.name
}
const styles = theme => ({
root: {
height: 250,
flexGrow: 1
},
container: {
position: 'relative'
},
suggestionsContainerOpen: {
position: 'absolute',
zIndex: 1000,
marginTop: theme.spacing.unit,
left: 0,
right: 0
},
suggestion: {
display: 'block'
},
suggestionsList: {
margin: 0,
padding: 0,
listStyleType: 'none'
},
divider: {
height: theme.spacing.unit * 2
}
})
class MetainfoSearch extends React.Component {
state = {
single: '',
popper: '',
suggestions: []
};
getSuggestions(value) {
const inputValue = deburr(value.trim()).toLowerCase()
const inputLength = inputValue.length
let count = 0
return inputLength === 0
? []
: this.props.suggestions.filter(suggestion => {
const keep =
count < 5 && suggestion.name.slice(0, inputLength).toLowerCase() === inputValue
if (keep) {
count += 1
}
return keep
})
}
handleSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: this.getSuggestions(value)
})
}
handleSuggestionsClearRequested = () => {
this.setState({
suggestions: []
})
}
handleChange = name => (event, { newValue }) => {
this.setState({
[name]: newValue
})
this.props.onChange(newValue)
}
render() {
const { classes } = this.props
const autosuggestProps = {
renderInputComponent,
suggestions: this.state.suggestions,
onSuggestionsFetchRequested: this.handleSuggestionsFetchRequested,
onSuggestionsClearRequested: this.handleSuggestionsClearRequested,
getSuggestionValue,
renderSuggestion
}
return (
<Autosuggest
{...autosuggestProps}
inputProps={{
classes: classes,
label: 'Definition',
placeholder: 'Enter definition name',
value: this.state.single,
onChange: this.handleChange('single'),
InputLabelProps: {
shrink: true
}
}}
theme={{
container: classes.container,
suggestionsContainerOpen: classes.suggestionsContainerOpen,
suggestionsList: classes.suggestionsList,
suggestion: classes.suggestion
}}
renderSuggestionsContainer={options => (
<Paper {...options.containerProps} square>
{options.children}
</Paper>
)}
/>
)
}
}
MetainfoSearch.propTypes = {
classes: PropTypes.object.isRequired,
suggestions: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired
}
export default withStyles(styles)(MetainfoSearch)
......@@ -14,7 +14,7 @@ class SectionFeature extends React.Component {
},
visible: {
extends: 'root',
backgroundColor: grey[200]
backgroundColor: grey[300]
},
gutters: {
paddingLeft: theme.spacing.unit,
......
......@@ -2,7 +2,6 @@ import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import grey from '@material-ui/core/colors/grey'
import blue from '@material-ui/core/colors/blue'
import { AfterRender } from './util/after-render'
import { Definitions } from './Definition'
import { updateList } from './util/data'
......@@ -12,17 +11,11 @@ class ViewerUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
rootElement: PropTypes.object.isRequired,
packages: PropTypes.arrayOf(PropTypes.object).isRequired,
visiblePackages: PropTypes.arrayOf(PropTypes.string).isRequired
packages: PropTypes.arrayOf(PropTypes.object).isRequired
}
static styles = (theme) => ({
root: {
},
packages: {
padding: theme.spacing.unit
},
root: {},
container: {
position: 'relative'
},
......@@ -32,7 +25,7 @@ class ViewerUnstyled extends React.Component {
flexWrap: 'nowrap',
justifyContent: 'flex-start',
alignItems: 'center',
padding: theme.spacing.unit,
padding: theme.spacing.unit * 2,
zIndex: 1
},
sankey: {
......@@ -43,11 +36,11 @@ class ViewerUnstyled extends React.Component {
zIndex: 0
},
references: {
fill: blue[200],
fill: theme.palette.primary[200],
fillOpacity: 0.8
},
containments: {
fill: grey[200],
fill: grey[300],
fillOpacity: 0.8
}
})
......@@ -60,10 +53,6 @@ class ViewerUnstyled extends React.Component {
this.isVisible = this.isVisible.bind(this)
this.state = {
packages: this.props.packages.map(pkg => ({
name: pkg.name,
visible: this.props.visiblePackages.find(name => pkg.name === name) !== undefined
})),
definitions: [{
definition: this.props.rootElement,
state: []
......@@ -219,11 +208,6 @@ class ViewerUnstyled extends React.Component {
const {classes} = this.props
return (
<div className={classes.root}>
{/* <div className={classes.packages}>
{this.state.packages.map(({name, visible}, index) => (
<CheckChip key={index} label={name} checked={visible}/>
))}
</div> */}
<div ref={this.canvasRef} className={classes.container}>
<AfterRender classes={{content: classes.canvas, afterRender: classes.sankey}} afterRender={this.renderSankey}>
<Definitions current={this.props.rootElement}
......
......@@ -221,13 +221,11 @@ class AfterRenderTestUnstyled extends React.Component {
render() {
const {classes} = this.props
return (
<div style={{padding: 10}}>
<AfterRender afterRender={this.afterRender} classes={{afterRender: classes.afterRender}}>
<AfterRenderMeasure measureId={1}>Hello</AfterRenderMeasure>
<AfterRenderMeasure measureId={2}>Hello World</AfterRenderMeasure>
<Button>click</Button>
</AfterRender>
</div>
<AfterRender afterRender={this.afterRender} classes={{afterRender: classes.afterRender}}>
<AfterRenderMeasure measureId={1}>Hello</AfterRenderMeasure>
<AfterRenderMeasure measureId={2}>Hello World</AfterRenderMeasure>
<Button>click</Button>
</AfterRender>
)
}
}
......
......@@ -17,13 +17,13 @@ The archive API of the nomad@FAIRDI APIs. This API is about serving processed
(parsed and normalized) calculation data in nomad's *meta-info* format.
"""
from typing import Dict, Any
import os.path
from flask import send_file
from flask_restplus import abort, Resource
import json
import nomad_meta_info
from nomadcore.local_meta_info import load_metainfo
from nomad.files import UploadFiles, Restricted
......@@ -111,21 +111,64 @@ class ArchiveCalcResource(Resource):
abort(404, message='Calculation %s does not exist.' % archive_id)
@ns.route('/metainfo/<string:metainfo_path>')
@api.doc(params=dict(nomad_metainfo_path='A path or metainfo definition file name.'))
@ns.route('/metainfo/<string:metainfo_package_name>')
@api.doc(params=dict(metainfo_package_name='The name of the metainfo package.'))
class MetainfoResource(Resource):
@api.doc('get_metainfo')
@api.response(404, 'The metainfo does not exist')
@api.response(200, 'Metainfo data send')
def get(self, metainfo_path):
def get(self, metainfo_package_name):
"""
Get a metainfo definition file.
"""
try: