Commit 85e448dd authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Implemented a view for archive data and added links to upload table.

parent dcd4bdbe
......@@ -56,9 +56,15 @@ function getUploads() {
.then(uploadsJson => uploadsJson.map(uploadJson => new Upload(uploadJson)))
}
function archive(uploadHash, calcHash) {
return fetch(`${apiBase}/archive/${uploadHash}/${calcHash}`)
.then(response => response.json())
}
const api = {
createUpload: createUpload,
getUploads: getUploads
getUploads: getUploads,
archive: archive
};
export default api;
\ No newline at end of file
import React from 'react';
import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
import deepPurple from '@material-ui/core/colors/deepPurple';
import orange from '@material-ui/core/colors/orange';
import { MuiThemeProvider } from '@material-ui/core/styles';
import { repoTheme } from '../config';
import Navigation from './Navigation';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import Uploads from './Uploads'
const theme = createMuiTheme({
palette: {
primary: deepPurple,
secondary: orange,
},
});
import ArchiveCalc from './ArchiveCalc';
function App() {
return (
<MuiThemeProvider theme={theme}>
<MuiThemeProvider theme={repoTheme}>
<BrowserRouter>
<Navigation>
<Switch>
<Route exact path="/" render={() => <div>Home</div>} />
<Route path="/browse" render={() => <div>Browse</div>} />
<Route path="/repo" render={() => <div>Browse</div>} />
<Route path="/upload" component={Uploads} />
<Route exact path="/archive" render={() => <div>Archive</div>} />
<Route path="/archive/:uploadHash/:calcHash" component={ArchiveCalc} />
<Route path="/profile" render={() => <div>Profile</div>} />
<Route path="/documentation" render={() => <div>Docs</div>} />
<Route render={() => <div>Not found</div>} />
......
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles, Paper, LinearProgress } from '@material-ui/core';
import ReactJson from 'react-json-view'
import api from '../api';
import Markdown from './Markdown';
class ArchiveCalc extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired
}
static styles = theme => ({
root: {},
calcData: {
padding: theme.spacing.unit
}
});
constructor(props) {
super(props)
this.state = {
data: null
}
}
componentDidMount() {
const { uploadHash, calcHash} = this.props.match.params
api.archive(uploadHash, calcHash).then(data => {
this.setState({data: data})
})
}
render() {
const { classes } = this.props
const { data } = this.state
return (
<div className={classes.root}>
<Markdown>{`
## The Archive – Code Independent Data
The tree below shows all calculation data in nomad's *hierachical* and
*code independent*. You can learn more about the different *sections* and
*quantities* by visiting the [metainfo](/metainfo).
`}</Markdown>
<Paper className={classes.calcData}>
{
data ?
<ReactJson src={this.state.data} enableClipboard={false} collapsed={4} /> :
<LinearProgress variant="query" />
}
</Paper>
</div>
)
}
}
export default withStyles(ArchiveCalc.styles)(ArchiveCalc);
\ No newline at end of file
......@@ -183,13 +183,18 @@ var styles = theme => ({
});
function Markdown(props) {
const { classes, text } = props;
const { classes, text, children } = props;
let content = text
if (children) {
content = children.replace(/^\s+/gm, '')
}
return (
<div>
<div
className={classes.root}
dangerouslySetInnerHTML={{__html: marked(text)}}
dangerouslySetInnerHTML={{__html: marked(content)}}
/>
</div>
)
......@@ -197,7 +202,8 @@ function Markdown(props) {
Markdown.propTypes = {
classes: PropTypes.object.isRequired,
text: PropTypes.string.isRequired
text: PropTypes.string,
children: PropTypes.string
};
export default withStyles(styles)(Markdown);
\ No newline at end of file
......@@ -15,18 +15,21 @@ import SearchIcon from '@material-ui/icons/Search';
import AccountIcon from '@material-ui/icons/AccountCircle';
import DocumentationIcon from '@material-ui/icons/Help';
import HomeIcon from '@material-ui/icons/Home';
import ArchiveIcon from '@material-ui/icons/Storage';
import { Link, withRouter } from 'react-router-dom';
import { compose } from 'recompose'
import { Avatar } from '@material-ui/core';
import { repoTheme, archiveTheme } from '../config';
const drawerWidth = 200;
const toolbarTitles = {
'/': 'Welcome',
'/browse': 'Search, View, and Download Data',
'/repo': 'Search, View, and Download Data',
'/upload': 'Upload Your Own Data',
'/profile': 'Your Profile',
'/documentation': 'Documentation'
'/documentation': 'Documentation',
'/archive': 'The Nomad Archive'
}
const styles = theme => ({
......@@ -86,18 +89,24 @@ function ClippedDrawer(props) {
</ListItemIcon>
<ListItemText inset primary="Home"/>
</MenuItem>
<MenuItem component={Link} to="/browse" selected={ '/browse' === pathname }>
<MenuItem component={Link} to="/repo" selected={ pathname.startsWith('/repo') }>
<ListItemIcon>
<SearchIcon />
<SearchIcon style={{fill: repoTheme.palette.primary.main}}/>
</ListItemIcon>
<ListItemText inset primary="Browse"/>
<ListItemText inset primary="Repository"/>
</MenuItem>
<MenuItem component={Link} to="/upload" selected={ '/upload' === pathname }>
<ListItemIcon>
<BackupIcon />
<BackupIcon style={{fill: repoTheme.palette.primary.main}}/>
</ListItemIcon>
<ListItemText inset primary="Upload"/>
</MenuItem>
<MenuItem component={Link} to="/archive" selected={ pathname.startsWith('/archive') }>
<ListItemIcon>
<ArchiveIcon style={{fill: archiveTheme.palette.primary.main}}/>
</ListItemIcon>
<ListItemText inset primary="Archive"/>
</MenuItem>
</MenuList>
<Divider/>
<MenuList>
......
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles, ExpansionPanel, ExpansionPanelSummary, Typography, ExpansionPanelDetails, Stepper, Step, StepLabel, Table, TableRow, TableCell } from '@material-ui/core';
import { Link } from 'react-router-dom';
import { withStyles, ExpansionPanel, ExpansionPanelSummary, Typography, ExpansionPanelDetails, Stepper, Step, StepLabel, Table, TableRow, TableCell, IconButton, MuiThemeProvider, TableBody } from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import RepoIcon from '@material-ui/icons/Cloud';
import ArchiveIcon from '@material-ui/icons/Storage';
import EncIcon from '@material-ui/icons/Assessment';
import ReactJson from 'react-json-view'
import { repoTheme, encTheme, archiveTheme } from '../config';
class Upload extends React.Component {
......@@ -31,6 +36,11 @@ class Upload extends React.Component {
stepper: {
width: '100%',
padding: 0
},
buttonCell: {
overflow: 'hidden',
whiteSpace: 'nowrap',
textAlign: 'right'
}
});
......@@ -48,7 +58,7 @@ class Upload extends React.Component {
console.debug(`Sucessfully updated upload ${upload.upload_id}.`)
console.assert(upload.proc, 'Uploads always must have a proc')
this.setState({upload: upload})
if (upload.proc.status != 'SUCCESS') {
if (upload.proc.status !== 'SUCCESS') {
this.updateUpload()
}
})
......@@ -80,7 +90,7 @@ class Upload extends React.Component {
const { calc_procs, task_names, current_task_name, status } = this.state.upload.proc
let activeStep = task_names.indexOf(current_task_name)
activeStep += (status == 'SUCCESS') ? 1 : 0
activeStep += (status === 'SUCCESS') ? 1 : 0
return (
<Stepper activeStep={activeStep} classes={{root: classes.stepper}}>
......@@ -119,7 +129,7 @@ class Upload extends React.Component {
}
const renderRow = (calcProc, index) => {
const { mainfile, calc_hash, parser_name, task_names, current_task_name } = calcProc
const { mainfile, calc_hash, parser_name, task_names, current_task_name, archive_id } = calcProc
return (
<TableRow key={index}>
<TableCell>
......@@ -132,7 +142,7 @@ class Upload extends React.Component {
</TableCell>
<TableCell>
<Typography>
{parser_name}
{parser_name.replace('parsers/', '')}
</Typography>
</TableCell>
<TableCell>
......@@ -146,13 +156,26 @@ class Upload extends React.Component {
</b>
</Typography>
</TableCell>
<TableCell className={classes.buttonCell}>
<MuiThemeProvider theme={repoTheme}>
<IconButton color="primary" component={Link} to={`/repo/${archive_id}`}><RepoIcon /></IconButton>
</MuiThemeProvider>
<MuiThemeProvider theme={archiveTheme}>
<IconButton color="primary" component={Link} to={`/archive/${archive_id}`}><ArchiveIcon /></IconButton>
</MuiThemeProvider>
<MuiThemeProvider theme={encTheme}>
<IconButton color="primary" component={Link} to={`/enc/${archive_id}`}><EncIcon /></IconButton>
</MuiThemeProvider>
</TableCell>
</TableRow>
)
}
return (
<Table>
{calc_procs.map(renderRow)}
<TableBody>
{calc_procs.map(renderRow)}
</TableBody>
</Table>
)
}
......
......@@ -6,15 +6,6 @@ import Dropzone from 'react-dropzone';
import api from '../api';
import Upload from './Upload'
const greetings = `
## Upload your own data to **nomad xt**
You can upload your own data. Have your code output ready in a popular archive
format (e.g. \`*.zip\` or \`*.tar.gz\`) and drop it below. Your upload can
comprise the output of multiple runs, even of different codes. Don't worry, nomad
will find it.
`
var styles = theme => ({
root: {
width: '100%',
......@@ -72,7 +63,14 @@ class Uploads extends React.Component {
return (
<div className={classes.root}>
<Markdown text={greetings}/>
<Markdown>{`
## Upload your own data to **nomad xt**
You can upload your own data. Have your code output ready in a popular archive
format (e.g. \`*.zip\` or \`*.tar.gz\`) and drop it below. Your upload can
comprise the output of multiple runs, even of different codes. Don't worry, nomad
will find it.
`}</Markdown>
<Paper>
<Dropzone
accept="application/zip"
......
export const apiBase = 'http://localhost:5000'
\ No newline at end of file
import repo from '@material-ui/core/colors/deepPurple';
import archive from '@material-ui/core/colors/teal';
import enc from '@material-ui/core/colors/amber';
import secondary from '@material-ui/core/colors/blueGrey';
import { createMuiTheme } from '@material-ui/core';
export const apiBase = 'http://localhost:5000'
export const repoTheme = createMuiTheme({
palette: {
primary: repo,
secondary: secondary,
}
});
export const archiveTheme = createMuiTheme({
palette: {
primary: archive,
secondary: secondary,
}
});
export const encTheme = createMuiTheme({
palette: {
primary: enc,
secondary: secondary,
}
});
\ No newline at end of file
from flask import Flask, Response, request
from flask import Flask, Response, request, redirect
from flask_restful import Resource, Api, abort
from datetime import datetime
import mongoengine.errors
......@@ -80,13 +80,13 @@ def get_calc(upload_hash, calc_hash):
archive_id = '%s/%s' % (upload_hash, calc_hash)
logger = get_logger(__name__, archive_id=archive_id)
try:
file = files.open_archive_json(archive_id)
return Response(file, mimetype='application/json', status=200)
url = files.archive_url(archive_id)
return redirect(url, 302)
except KeyError:
abort(404, message='Archive %s does not exist.' % archive_id)
except Exception as e:
logger.error('Exception on reading archive', exc_info=e)
abort(500, message='Could not read the archive.')
logger.error('Exception on accessing archive', exc_info=e)
abort(500, message='Could not accessing the archive.')
api.add_resource(Uploads, '/uploads')
......
......@@ -298,6 +298,17 @@ def write_archive_json(archive_id) -> Generator[TextIO, None, None]:
binary_out.close()
def archive_url(archive_id) -> str:
""" Returns the file server url for the archive. """
try:
_client.stat_object(config.files.archive_bucket, archive_id)
except minio.error.NoSuchKey:
raise KeyError()
return 'http://%s:%d/%s/%s' % \
(config.minio.host, config.minio.port, config.files.archive_bucket, archive_id)
def open_archive_json(archive_id) -> IO:
""" Returns a file-like to read the archive json. """
# The result already is a file-like and due to the Content-Encoding metadata is
......
......@@ -16,12 +16,12 @@ from typing import List, Any, Union, cast
from celery.result import AsyncResult, result_from_tuple
import itertools
from nomad import utils
from nomad.normalizing import normalizers
from nomad.utils import DataObject
from nomad.processing.app import app
class ProcPipeline(DataObject):
class ProcPipeline(utils.DataObject):
"""
Arguments:
task_names: A list of task names in pipeline order.
......@@ -111,7 +111,7 @@ class CalcProc(ProcPipeline):
self.parser_name = parser_name
self.tmp_mainfile = tmp_mainfile
self.calc_hash = hash(mainfile)
self.calc_hash = utils.hash(mainfile)
self.archive_id = '%s/%s' % (self.upload_hash, self.calc_hash)
self.celery_task_id: str = None
......@@ -119,7 +119,8 @@ class CalcProc(ProcPipeline):
self.update(kwargs)
def update_from_backend(self):
assert self.celery_task_id is not None
if self.celery_task_id is None:
return
celery_task_result = AsyncResult(self.celery_task_id, app=app)
if celery_task_result.ready():
......
......@@ -152,7 +152,7 @@ def test_processing(client, file, celery_session_worker):
def test_get_archive(client, archive_id):
rv = client.get('/archive/%s' % archive_id)
assert rv.status_code == 200
assert rv.status_code == 302
def test_get_non_existing_archive(client):
......
......@@ -139,6 +139,13 @@ def test_hash(uploaded_id: str):
assert isinstance(hash, str)
def test_archive_url(archive_id: str):
result = files.archive_url(archive_id)
assert result is not None
assert result.startswith('http')
def test_archive(archive_id: str):
result = json.load(files.open_archive_json(archive_id))
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment