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

Merge branch 'v0.7.2' into 'master'

Release v0.7.2

See merge request !79
parents 13700c66 e42036af
Pipeline #68276 canceled with stages
in 18 seconds
......@@ -29,6 +29,10 @@ contributing, and API reference.
Omitted versions are plain bugfix releases with only minor changes and fixes.
### v0.7.2
- API curl, Python, and results for entries and search queries shown in the GUI
- minor bugfixes
### v0.7.1
- Download of archive files based on search queries
- minor bugfixes
......
......@@ -11,6 +11,7 @@ from urllib.parse import urlparse, urlencode
import requests
import re
import time
import os
import os.path
import tarfile
import io
......@@ -24,6 +25,7 @@ user = 'leonard.hofstadter@nomad-fairdi.tests.de'
password = 'password'
approx_upload_size = 32 * 1024 * 1024 * 1024 # you can make it really small for testing
max_parallel_uploads = 9
direct_stream = False
# create the bravado client
host = urlparse(nomad_url).netloc.split(':')[0]
......@@ -115,8 +117,6 @@ def upload_next_data(sources: Iterator[Tuple[str, str, str]], upload_name='next
zip_stream = zipstream.ZipFile(mode='w', compression=zipfile.ZIP_STORED, allowZip64=True)
zip_stream.paths_to_write = iterator()
zip_stream
user = client.auth.get_user().response().result
token = user.token
url = nomad_url + '/uploads/?%s' % urlencode(dict(name=upload_name))
......@@ -126,8 +126,21 @@ def upload_next_data(sources: Iterator[Tuple[str, str, str]], upload_name='next
if len(chunk) != 0:
yield chunk
# stream .zip to nomad
response = requests.put(url=url, headers={'X-Token': token, 'Content-type': 'application/octet-stream'}, data=content())
if direct_stream:
# stream .zip to nomad
response = requests.put(url=url, headers={'X-Token': token, 'Content-type': 'application/octet-stream'}, data=content())
else:
# save .zip and upload file to nomad
zipfile_name = '/tmp/%s.zip' % str(uuid.uuid4())
with open(zipfile_name, 'wb') as f:
for c in content():
f.write(c)
try:
with open(zipfile_name, 'rb') as f:
response = requests.put(url=url, headers={'X-Token': token, 'Content-type': 'application/octet-stream'}, data=f)
finally:
os.remove(zipfile_name)
if response.status_code != 200:
raise Exception('nomad return status %d' % response.status_code)
......
......@@ -3,6 +3,5 @@ window.nomadEnv = {
'keycloakRealm': 'fairdi_nomad_test',
'keycloakClientId': 'nomad_gui_dev',
'appBase': 'http://localhost:8000/fairdi/nomad/latest',
'kibanaBase': '/fairdi/kibana',
'debug': false
}
......@@ -8,6 +8,7 @@
</script>
<script src="https://unpkg.com/pace-js@1.0.2/pace.min.js"></script>
<link href="%PUBLIC_URL%/pace.css" rel="stylesheet" />
<link href="%PUBLIC_URL%/main.css" rel="stylesheet" />
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
......@@ -35,7 +36,7 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<title>NOMAD upload</title>
</head>
<body style="overflow-y: hidden;">
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
......
body {
overflow-y: hidden;
}
\ No newline at end of file
......@@ -2,7 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Markdown from './Markdown'
import { kibanaBase, appBase, optimadeBase, apiBase, debug, consent } from '../config'
import { appBase, optimadeBase, apiBase, debug, consent } from '../config'
import { compose } from 'recompose'
import { withApi } from './api'
import { withDomain } from './domains'
......@@ -74,7 +74,7 @@ class About extends React.Component {
(previously called *Elastic Logstash Kibana* (ELK)-stack).
This system pushes logs, events, monitoring data,
and other application metrics to a central database where it
can be analysed visually. Here is the [link to Kibana](${kibanaBase}/)
can be analysed visually by us.
### Test user
During development this GUI might not be connected to the actual NOMAD
......@@ -90,7 +90,7 @@ class About extends React.Component {
- domain: ${info ? info.domain.name : 'loading'}
- git: \`${info ? info.git.ref : 'loading'}; ${info ? info.git.version : 'loading'}\`
- last commit message: *${info ? info.git.log : 'loading'}*
- codes: ${info ? info.codes.join(', ') : 'loading'}
- supported codes: ${info ? info.codes.join(', ') : 'loading'}
- parsers: ${info ? info.parsers.join(', ') : 'loading'}
- normalizers: ${info ? info.normalizers.join(', ') : 'loading'}
`}</Markdown>
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core'
import { withStyles, IconButton, Dialog, DialogTitle, DialogContent, DialogActions, Button, Tooltip, Typography } from '@material-ui/core'
import CodeIcon from '@material-ui/icons/Code'
import ReactJson from 'react-json-view'
import Markdown from './Markdown'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import ClipboardIcon from '@material-ui/icons/Assignment'
class ApiDialogUnstyled extends React.Component {
static propTypes = {
......@@ -16,49 +19,98 @@ class ApiDialogUnstyled extends React.Component {
content: {
paddingBottom: 0
},
raw: {
margin: 0, padding: 0
json: {
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2
},
codeContainer: {
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-start'
},
code: {
flexGrow: 1,
marginRight: theme.spacing.unit,
overflow: 'hidden'
},
codeActions: {
marginTop: theme.spacing.unit * 3
}
})
state = {
showRaw: false
}
constructor(props) {
super(props)
this.handleToggleRaw = this.handleToggleRaw.bind(this)
}
handleToggleRaw() {
this.setState({showRaw: !this.state.showRaw})
}
render() {
const { classes, title, data, onClose, ...dialogProps } = this.props
const { showRaw } = this.state
return (
<Dialog {...dialogProps}>
<DialogTitle>{title || 'API'}</DialogTitle>
<Dialog maxWidth="lg" fullWidth {...dialogProps}>
<DialogTitle>{title || 'API Code'}</DialogTitle>
<DialogContent classes={{root: classes.content}}>
{showRaw
? <code>
<pre className={classes.raw}>
{JSON.stringify(data, null, 4)}
</pre>
</code> : <ReactJson
src={data}
enableClipboard={false}
collapsed={2}
displayObjectSize={false}
/>
}
<Typography>Access the archive as JSON via <i>curl</i>:</Typography>
<div className={classes.codeContainer}>
<div className={classes.code}>
<Markdown>{`
\`\`\`
${data.curl}
\`\`\`
`}</Markdown>
</div>
<div className={classes.codeActions}>
<CopyToClipboard text={data.curl} onCopy={() => null}>
<Tooltip title="Copy to clipboard">
<IconButton>
<ClipboardIcon />
</IconButton>
</Tooltip>
</CopyToClipboard>
</div>
</div>
<Typography>Access the archive in <i>python</i>:</Typography>
<div className={classes.codeContainer}>
<div className={classes.code}>
<Markdown>{`
\`\`\`
${data.python}
\`\`\`
`}</Markdown>
</div>
<div className={classes.codeActions}>
<CopyToClipboard text={data.python} onCopy={() => null}>
<Tooltip title="Copy to clipboard">
<IconButton>
<ClipboardIcon />
</IconButton>
</Tooltip>
</CopyToClipboard>
</div>
</div>
<Typography>The repository API response as JSON:</Typography>
<div className={classes.codeContainer}>
<div className={classes.code}>
<div className={classes.json}>
<ReactJson
src={data}
enableClipboard={false}
collapsed={2}
displayObjectSize={false}
/>
</div>
</div>
<div className={classes.codeActions}>
<CopyToClipboard text={data} onCopy={() => null}>
<Tooltip title="Copy to clipboard">
<IconButton>
<ClipboardIcon />
</IconButton>
</Tooltip>
</CopyToClipboard>
</div>
</div>
</DialogContent>
<DialogActions>
<Button onClick={this.handleToggleRaw}>
{showRaw ? 'show tree' : 'show raw JSON'}
</Button>
<Button onClick={onClose}>
Close
</Button>
......@@ -101,9 +153,11 @@ class ApiDialogButtonUnstyled extends React.Component {
return (
<div className={classes.root}>
{component ? component({onClick: this.handleShowDialog}) : <IconButton onClick={this.handleShowDialog}>
<CodeIcon />
</IconButton>
{component ? component({onClick: this.handleShowDialog}) : <Tooltip title="Show API code">
<IconButton onClick={this.handleShowDialog}>
<CodeIcon />
</IconButton>
</Tooltip>
}
<ApiDialog
{...dialogProps} open={showDialog}
......
......@@ -28,7 +28,7 @@ import {help as metainfoHelp, default as MetaInfoBrowser} from './metaInfoBrowse
import packageJson from '../../package.json'
import { Cookies, withCookies } from 'react-cookie'
import Markdown from './Markdown'
import {help as uploadHelp, default as Uploads} from './uploads/Uploads'
import {help as uploadHelp, default as UploadPage} from './uploads/UploadPage'
import ResolvePID from './entry/ResolvePID'
import DatasetPage from './DatasetPage'
import { capitalize } from '../utils'
......@@ -456,7 +456,7 @@ export default class App extends React.Component {
exact: true,
singleton: true,
path: '/uploads',
render: props => <Uploads {...props} />
render: props => <UploadPage {...props} />
},
'metainfo': {
exact: true,
......
......@@ -83,7 +83,7 @@ class DataTableToolbarUnStyled extends React.Component {
const { anchorEl } = this.state
const open = Boolean(anchorEl)
const regularActions = <React.Fragment>
const regularActions = <React.Fragment>
{actions || <React.Fragment/>}
<Tooltip title="Change displayed columns">
<IconButton onClick={this.handleClick}>
......@@ -243,6 +243,10 @@ class DataTableUnStyled extends React.Component {
paddingLeft: theme.spacing.unit * 3,
paddingRight: theme.spacing.unit * 3
},
ellipsisFront: {
direction: 'rtl',
textAlign: 'left'
},
clickable: {
cursor: 'pointer'
},
......@@ -261,17 +265,7 @@ class DataTableUnStyled extends React.Component {
width: 1
},
details: {
borderBottom: '1px solid rgba(224, 224, 224, 1)',
padding: theme.spacing.unit * 3
},
detailsContentsWithActions: {
paddingTop: theme.spacing.unit * 3,
paddingLeft: theme.spacing.unit * 3,
paddingRight: theme.spacing.unit * 3
},
detailsActions: {
textAlign: 'right',
padding: theme.spacing.unit
borderBottom: '1px solid rgba(224, 224, 224, 1)'
},
selectedEntryCell: {
color: theme.palette.primary.contrastText,
......@@ -489,9 +483,12 @@ class DataTableUnStyled extends React.Component {
</TableCell> : <React.Fragment/> }
{Object.keys(columns).filter(key => selectedColumns.indexOf(key) !== -1).map((key, i) => {
const column = columns[key]
if (column.ellipsisFront) {
console.log('####################')
}
return (
<TableCell
className={clsx([classes.cell, (selectedEntry === rowId) && classes.selectedEntryCell])}
className={clsx([classes.cell, column.ellipsisFront && classes.ellipsisFront, (selectedEntry === rowId) && classes.selectedEntryCell])}
key={key}
align={column.align || 'left'}
>
......
......@@ -57,7 +57,7 @@ class DownloadButton extends React.Component {
raiseError(e)
}
}
FileSaver.saveAs(`${apiBase}/${choice}/query?${new URLSearchParams(params).toString()}`, `nomad-${choice}-download.zip`)
FileSaver.saveAs(`${apiBase}/${choice}/${choice === 'archive' ? 'download' : 'query'}?${new URLSearchParams(params).toString()}`, `nomad-${choice}-download.zip`)
this.setState({preparingDownload: false, anchorEl: null})
}
......
......@@ -176,8 +176,8 @@ class MyAutosuggestUnstyled extends React.PureComponent {
ref(node)
inputRef(node)
},
name: 'search', // try to prevent browsers ignore autocomplete="off"
type: 'search', // try to prevent browsers ignore autocomplete="off"
name: 'search', // try to prevent browsers ignore autocomplete="off"
type: 'search', // try to prevent browsers ignore autocomplete="off"
classes: {
input: classes.input
}
......@@ -397,7 +397,7 @@ class ReferenceInput extends React.Component {
return <TextField
fullWidth
{...rest}
type="search" name="search" // attempt to avoid browsers autofill, since they seem to ignore autocomplete="off"
type="search" name="search" // attempt to avoid browsers autofill, since they seem to ignore autocomplete="off"
value={this.state.inputValue}
onChange={this.handleChange.bind(this)}
error={value === undefined}
......@@ -582,10 +582,10 @@ class InviteUserDialogUnstyled extends React.Component {
submitEnabled: false
}
state = this.defaultState
state = {...this.defaultState}
handleClose() {
this.setState({open: false})
this.setState({...this.defaultState, open: false})
}
handleSubmit() {
......@@ -595,8 +595,16 @@ class InviteUserDialogUnstyled extends React.Component {
this.handleClose()
}).catch(error => {
// get message in quotes
const message = ('' + error).match(/'([^']+)'/)[1]
this.setState({error: message, submitting: false, submitEnabled: false})
console.error(error)
try {
let message = ('' + error).match(/'([^']+)'/)[1]
try {
message = JSON.parse(message).errorMessage
} catch (e) {}
this.setState({error: message, submitting: false, submitEnabled: false})
} catch (e) {
this.setState({error: '' + error, submitting: false, submitEnabled: false})
}
})
}
......@@ -1051,7 +1059,7 @@ class EditUserMetadataDialogUnstyled extends React.Component {
if (submitting) {
return <DialogActions>
<DialogContentText color="error" style={{marginLeft: 16}}>Do not close the page. This might take up to several minutes for editing many entries.</DialogContentText>
<DialogContentText style={{marginLeft: 16}}>Do not close the page. This might take up to several minutes for editing many entries.</DialogContentText>
<span style={{flexGrow: 1}} />
<div className={classes.submitWrapper}>
<Button onClick={this.handleSubmit} disabled={!submitEnabled} color="primary">
......
......@@ -17,7 +17,8 @@ class Quantity extends React.Component {
column: PropTypes.bool,
data: PropTypes.object,
quantity: PropTypes.string,
withClipboard: PropTypes.bool
withClipboard: PropTypes.bool,
ellipsisFront: PropTypes.bool
}
static styles = theme => ({
......@@ -30,6 +31,15 @@ class Quantity extends React.Component {
value: {
flexGrow: 1
},
ellipsis: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
},
ellipsisFront: {
direction: 'rtl',
textAlign: 'left'
},
valueAction: {},
valueActionButton: {
padding: 4
......@@ -50,24 +60,35 @@ class Quantity extends React.Component {
'& > :not(:first-child)': {
marginTop: theme.spacing.unit * 1
}
},
label: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: '0.75rem',
fontWeight: 500
}
})
render() {
const {classes, children, label, typography, loading, placeholder, noWrap, row, column, quantity, data, withClipboard} = this.props
const {classes, children, label, typography, loading, placeholder, noWrap, row, column, quantity, data, withClipboard, ellipsisFront} = this.props
let content = null
let clipboardContent = null
let valueClassName = classes.value
if (noWrap && ellipsisFront) {
valueClassName = `${valueClassName} ${classes.ellipsisFront}`
}
console.log(valueClassName)
if (!loading) {
if (!(data && quantity && !data[quantity])) {
if (!children || children.length === 0) {
const value = data && quantity ? data[quantity] : null
if (value) {
clipboardContent = value
content = <Typography noWrap={noWrap} variant={typography} className={classes.value}>
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
{value}
</Typography>
} else {
content = <Typography noWrap={noWrap} variant={typography} className={classes.value}>
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>{placeholder || 'unavailable'}</i>
</Typography>
}
......@@ -75,7 +96,7 @@ class Quantity extends React.Component {
content = children
}
} else {
content = <Typography noWrap={noWrap} variant={typography} className={classes.value}>
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>{placeholder || 'unavailable'}</i>
</Typography>
}
......@@ -86,10 +107,10 @@ class Quantity extends React.Component {
} else {
return (
<div className={classes.root}>
<Typography noWrap variant="caption">{label || quantity}</Typography>
<Typography noWrap classes={{root: classes.label}} variant="caption">{label || quantity}</Typography>
<div className={classes.valueContainer}>
{loading
? <Typography noWrap={noWrap} variant={typography} className={classes.value}>
? <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>loading ...</i>
</Typography> : content}
{withClipboard
......
......@@ -586,6 +586,9 @@ export class ApiProviderComponent extends React.Component {
api.getInfo()
.catch(handleApiError)
.then(info => {
if (info.parsers) {
info.parsers.sort()
}
this.setState({info: info})
})
.catch(error => {
......
......@@ -100,7 +100,7 @@ class RepoEntryView extends React.Component {
<Quantity column>
<Quantity quantity='comment' placeholder='no comment' {...quantityProps} />
<Quantity quantity='references' placeholder='no references' {...quantityProps}>
<div style={{display:'inline-grid'}}>
<div style={{display: 'inline-grid'}}>
{(calcData.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
......@@ -138,7 +138,7 @@ class RepoEntryView extends React.Component {
{new Date(calcData.upload_time * 1000).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity='mainfile' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity='mainfile' loading={loading} noWrap ellipsisFront {...quantityProps} withClipboard />
<Quantity quantity="calc_hash" label={`${domain.entryLabel} hash`} loading={loading} noWrap {...quantityProps} />
<Quantity quantity="raw_id" label='raw id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="external_id" label='external id' loading={loading} noWrap {...quantityProps} withClipboard />
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, Link, Typography, Tooltip, IconButton, TablePagination } from '@material-ui/core'
import { withStyles, Link, Typography, Tooltip, IconButton, TablePagination, Button } from '@material-ui/core'
import { compose } from 'recompose'
import { withRouter } from 'react-router'
import { withDomain } from '../domains'
......@@ -36,10 +36,26 @@ export class EntryListUnstyled extends React.Component {
overflow: 'auto'
},
entryDetails: {
paddingTop: theme.spacing.unit * 3,
paddingLeft: theme.spacing.unit * 3,
paddingRight: theme.spacing.unit * 3
},
entryDetailsContents: {
display: 'flex'
},
entryDetailsRow: {
paddingRight: theme.spacing.unit * 3
},
entryDetailsActions: {
display: 'flex',
flexBasis: 'auto',
flexGrow: 0,
flexShrink: 0,
justifyContent: 'flex-end',
marginBottom: theme.spacing.unit,
marginLeft: theme.spacing.unit / 2,
marginRight: theme.spacing.unit / 2,
marginTop: theme.spacing.unit
}
})
......@@ -52,7 +68,8 @@ export class EntryListUnstyled extends React.Component {
label: 'Mainfile',
render: entry => entry.mainfile,
supportsSort: true,
description: 'The mainfile of this entry.'
ellipsisFront: true,
description: 'The mainfile of this entry within its upload.'
},
upload_time: {
label: 'Upload time',
......@@ -170,82 +187,82 @@ export class EntryListUnstyled extends React.Component {
renderEntryDetails(row) {
const { classes, domain } = this.props
return (<div className={classes.entryDetails}>
<div className={classes.entryDetailsRow}>
<Quantity column>
<Quantity row>
<Quantity quantity="formula" label='formula' noWrap data={row} />
</Quantity>
<Quantity row>
<Quantity quantity="code_name" label='dft code' noWrap data={row} />
<Quantity quantity="code_version" label='dft code version' noWrap data={row} />
</Quantity>