Commit 0600823c authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Various refactorings and fixes in GUI due to latest API changes.

parent 278fb1f7
Pipeline #41239 failed with stages
in 17 minutes and 32 seconds
......@@ -24,10 +24,9 @@
"url-parse": "^1.4.3"
},
"scripts": {
"metainfo": "git clone --single-branch -b nomad-fair http://gitlab.mpcdf.mpg.de/nomad-lab/nomad-meta-info.git --depth=1 public/metainfo",
"gitinfo": "echo \"{ \\\"log\\\": \\\"$(git log -1 --oneline)\\\", \\\"ref\\\": \\\"$(git describe --all)\\\", \\\"version\\\": \\\"$(git describe)\\\" }\" > src/gitinfo.json",
"start": "yarn metainfo; react-scripts start",
"build": "yarn metainfo; react-scripts build",
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
},
......
import { UploadRequest } from '@navjobs/upload'
import Swagger from 'swagger-client'
import { apiBase, appStaticBase } from './config'
import { apiBase } from './config'
const auth_headers = {
Authorization: 'Basic ' + btoa('sheldon.cooper@nomad-fairdi.tests.de:password')
......@@ -19,29 +19,31 @@ const swaggerPromise = Swagger(`${apiBase}/swagger.json`, {
})
const networkError = (e) => {
console.log(e)
throw Error('Network related error, cannot reach API: ' + e)
}
const handleJsonErrors = () => {
throw Error('Server return unexpected data format.')
const handleJsonErrors = (e) => {
console.log(e)
throw Error('API return unexpected data format.')
}
const handleResponseErrors = (response) => {
if (!response.ok) {
return response.json()
.catch(() => {
throw Error(`API/object storage error (${response.status}): ${response.statusText}`)
throw Error(`API error (${response.status}): ${response.statusText}`)
}).then(data => {
throw Error(`API/object storage error (${response.status}): ${data.message}`)
throw Error(`API error (${response.status}): ${data.message}`)
})
}
return response
}
class Upload {
constructor(json, created) {
constructor(json) {
this.uploading = 0
this._assignFromJson(json, created)
this._assignFromJson(json)
}
uploadFile(file) {
......@@ -58,7 +60,6 @@ class Upload {
},
files: [file],
progress: value => {
console.log(value)
this.uploading = value
}
}
......@@ -77,41 +78,39 @@ class Upload {
.then(() => this)
}
_assignFromJson(uploadJson, created) {
_assignFromJson(uploadJson) {
Object.assign(this, uploadJson)
if (this.current_task !== this.tasks[0]) {
this.uploading = 100
this.waiting = false
} else {
this.waiting = true
if (this.calcs) {
this.calcs.results.forEach(calc => {
const archiveId = calc.archive_id.split('/')
calc.upload_hash = archiveId[0]
calc.calc_hash = archiveId[1]
})
}
}
get(page, perPage, orderBy, order) {
if (!page) page = 1
if (!perPage) perPage = 5
if (!orderBy) orderBy = 'mainfile'
if (!order) order = 'desc'
order = order === 'desc' ? -1 : 1
if (this.uploading !== null && this.uploading !== 100) {
return new Promise(resolve => resolve(this))
} else {
const qparams = `page=${page}&per_page=${perPage}&order_by=${orderBy}&order=${order}`
return fetch(
`${apiBase}/uploads/${this.upload_id}?${qparams}`,
{
method: 'GET',
headers: auth_headers
})
if (this.upload_id) {
return swaggerPromise.then(client => client.apis.uploads.get_upload({
upload_id: this.upload_id,
page: page || 1,
per_page: perPage || 5,
order_by: orderBy || 'mainfile',
order: order || -1
}))
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(response => response.body)
.then(uploadJson => {
this._assignFromJson(uploadJson)
return this
})
} else {
return new Promise(resolve => resolve(this))
}
}
}
}
......@@ -119,33 +118,43 @@ class Upload {
function createUpload(name) {
return new Upload({
name: name,
tasks: ['UPLOADING'],
current_task: 'UPLOADING'
tasks: ['uploading'],
current_task: 'uploading',
uploading: 0,
create_time: new Date()
}, true)
}
function getUploads() {
return fetch(
`${apiBase}/uploads/`,
{
method: 'GET',
headers: auth_headers
})
async function getUploads() {
const client = await swaggerPromise
return client.apis.uploads.get_uploads()
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(uploadsJson => uploadsJson.map(uploadJson => new Upload(uploadJson)))
.then(response => response.body.map(uploadJson => {
const upload = new Upload(uploadJson)
upload.uploading = 100
return upload
}))
}
function archive(uploadHash, calcHash) {
return fetch(archiveUrl(uploadHash, calcHash))
async function archive(uploadHash, calcHash) {
const client = await swaggerPromise
return client.apis.archive.get_archive_calc({
upload_hash: uploadHash,
calc_hash: calcHash
})
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(response => response.body)
}
function calcProcLog(archiveId) {
return fetch(`${apiBase}/archive/logs/${archiveId}`)
async function calcProcLog(uploadHash, calcHash) {
const client = await swaggerPromise
console.log(uploadHash + calcHash)
return client.apis.archive.get_archive_logs({
upload_hash: uploadHash,
calc_hash: calcHash
})
.catch(networkError)
.then(response => {
if (!response.ok) {
......@@ -155,62 +164,53 @@ function calcProcLog(archiveId) {
return handleResponseErrors(response)
}
} else {
return response.text()
return response.text
}
})
}
function archiveUrl(uploadHash, calcHash) {
return `${apiBase}/archive/${uploadHash}/${calcHash}`
}
function repo(uploadHash, calcHash) {
return fetch(`${apiBase}/repo/${uploadHash}/${calcHash}`)
async function repo(uploadHash, calcHash) {
const client = await swaggerPromise
return client.apis.repo.get_repo_calc({
upload_hash: uploadHash,
calc_hash: calcHash
})
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(response => response.body)
}
function repoAll(page, perPage, owner) {
return fetch(
`${apiBase}/repo/?page=${page}&per_page=${perPage}&owner=${owner || 'all'}`,
{
method: 'GET',
headers: auth_headers
async function repoAll(page, perPage, owner) {
const client = await swaggerPromise
return client.apis.repo.get_calcs({
page: page,
per_page: perPage,
ower: owner || 'all'
})
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(response => response.body)
}
function deleteUpload(uploadId) {
return fetch(
`${apiBase}/uploads/${uploadId}`,
{
method: 'DELETE',
headers: auth_headers
})
async function deleteUpload(uploadId) {
const client = await swaggerPromise
return client.apis.uploads.delete_upload({upload_id: uploadId})
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(response => response.body)
}
function unstageUpload(uploadId) {
return fetch(
`${apiBase}/uploads/${uploadId}`,
{
method: 'POST',
body: JSON.stringify({
async function unstageUpload(uploadId) {
const client = await swaggerPromise
return client.apis.uploads.exec_upload_command({
upload_id: uploadId,
payload: {
operation: 'unstage'
}),
headers: {
'Content-Type': 'application/json',
...auth_headers
}
})
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.then(response => response.body)
}
let cachedMetaInfo = null
......@@ -220,7 +220,7 @@ async function getMetaInfo() {
return cachedMetaInfo
} else {
const loadMetaInfo = async(path) => {
return fetch(`${appStaticBase}/metainfo/meta_info/nomad_meta_info/${path}`)
return fetch(`${apiBase}/archive/metainfo/${path}`)
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
......@@ -262,7 +262,6 @@ const api = {
getUploads: getUploads,
archive: archive,
calcProcLog: calcProcLog,
archiveUrl: archiveUrl,
repo: repo,
repoAll: repoAll,
getMetaInfo: getMetaInfo
......
......@@ -103,8 +103,7 @@ class ArchiveCalc extends React.Component {
*quantities* by visiting the [meta-info](/metainfo) browser.
The tree below shows all calculation data in nomad's *hierachical* and
*code independent* archive format. You can download it
[here](${api.archiveUrl(uploadHash, calcHash)}). Click on values to
*code independent* archive format. Click on values to
see a *meta-info* description.
`}</Markdown>
<Typography className={classes.logLink}>
......@@ -124,7 +123,8 @@ class ArchiveCalc extends React.Component {
</Paper>
<CalcProcLogPopper
open={this.state.showLogs}
archiveId={`${uploadHash}/${calcHash}`}
uploadHash={uploadHash}
calcHash={calcHash}
onClose={() => this.setState({showLogs: false})}
anchorEl={this.logPopperAnchor.current}
raiseError={this.props.raiseError}
......
......@@ -10,7 +10,6 @@ import Link from 'react-router-dom/Link'
class CalcLink extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
calcId: PropTypes.string,
uploadHash: PropTypes.string,
calcHash: PropTypes.string,
disabled: PropTypes.bool
......@@ -24,19 +23,18 @@ class CalcLink extends React.Component {
});
render() {
const { uploadHash, calcHash, classes, calcId, disabled } = this.props
const id = calcId || `${uploadHash}/${calcHash}`
const { uploadHash, calcHash, classes, disabled } = this.props
return (
<div className={classes.root}>
<MuiThemeProvider theme={repoTheme}>
<IconButton color="primary" component={Link} to={`/repo/${id}`} disabled={disabled}><RepoIcon /></IconButton>
<IconButton color="primary" component={Link} to={`/repo/${uploadHash}/${calcHash}`} disabled={disabled}><RepoIcon /></IconButton>
</MuiThemeProvider>
<MuiThemeProvider theme={archiveTheme}>
<IconButton color="primary" component={Link} to={`/archive/${id}`} disabled={disabled}><ArchiveIcon /></IconButton>
<IconButton color="primary" component={Link} to={`/archive/${uploadHash}/${calcHash}`} disabled={disabled}><ArchiveIcon /></IconButton>
</MuiThemeProvider>
<MuiThemeProvider theme={encTheme}>
<IconButton color="primary" component={Link} to={`/enc/${id}`} disabled={disabled}><EncIcon /></IconButton>
<IconButton color="primary" component={Link} to={`/enc/${uploadHash}/${calcHash}`} disabled={disabled}><EncIcon /></IconButton>
</MuiThemeProvider>
</div>
)
......
......@@ -11,7 +11,8 @@ class CalcProcLogPopper extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
archiveId: PropTypes.string.isRequired,
uploadHash: PropTypes.string.isRequired,
calcHash: PropTypes.string.isRequired,
open: PropTypes.bool,
onClose: PropTypes.func,
anchorEl: PropTypes.any
......@@ -28,8 +29,8 @@ class CalcProcLogPopper extends React.Component {
}
componentDidMount() {
const {archiveId} = this.props
api.calcProcLog(archiveId).then(logs => {
const {uploadHash, calcHash} = this.props
api.calcProcLog(uploadHash, calcHash).then(logs => {
if (logs && logs !== '') {
this.setState({logs: logs})
}
......
......@@ -80,7 +80,7 @@ class Upload extends React.Component {
orderBy: 'status',
order: 'asc'
},
archiveLogs: null, // archive id of archive to show logs for
archiveLogs: null, // { uploadHash, calcHash } ids of archive to show logs for
loading: true, // its loading data from the server and the user should know about it
updating: true // it is still not complete and continieusly looking for updates
}
......@@ -94,7 +94,7 @@ class Upload extends React.Component {
const {page, perPage, orderBy, order} = params
this.setState({loading: true})
this.state.upload.get(page, perPage, orderBy, order)
this.state.upload.get(page, perPage, orderBy, order === 'asc' ? 1 : -1)
.then(upload => {
if (!this._unmounted) {
const continueUpdating = upload.status !== 'SUCCESS' && upload.status !== 'FAILURE' && !upload.is_stale
......@@ -169,7 +169,7 @@ class Upload extends React.Component {
renderStepper() {
const { classes } = this.props
const { upload } = this.state
const { calcs, tasks, current_task, status, errors, waiting } = upload
const { calcs, tasks, current_task, status, errors } = upload
let activeStep = tasks.indexOf(current_task)
activeStep += (status === 'SUCCESS') ? 1 : 0
......@@ -181,7 +181,7 @@ class Upload extends React.Component {
if (upload.status !== 'FAILURE') {
props.optional = (
<Typography variant="caption">
{waiting ? 'waiting for upload' : `${uploading || 0}%`}
{uploading === 100 && current_task === tasks[0] ? 'waiting for processing ...' : `${uploading || 0}%`}
</Typography>
)
}
......@@ -292,7 +292,7 @@ class Upload extends React.Component {
}
const renderRow = (calc, index) => {
const { mainfile, archive_id, parser, tasks, current_task, status, errors } = calc
const { mainfile, calc_hash, upload_hash, parser, tasks, current_task, status, errors } = calc
const color = status === 'FAILURE' ? 'error' : 'default'
const row = (
<TableRow key={index}>
......@@ -301,7 +301,7 @@ class Upload extends React.Component {
{mainfile}
</Typography>
<Typography variant="caption" color={color}>
{archive_id}
{upload_hash}/{calc_hash}
</Typography>
</TableCell>
<TableCell>
......@@ -324,7 +324,7 @@ class Upload extends React.Component {
<Typography color={color}>
{(status === 'SUCCESS' || status === 'FAILURE')
?
<a className={classes.logLink} href="#logs" onClick={() => this.setState({archiveLogs: archive_id})}>
<a className={classes.logLink} href="#logs" onClick={() => this.setState({archiveLogs: { uploadHash: upload_hash, calcHash: calc_hash }})}>
{status.toLowerCase()}
</a>
: status.toLowerCase()
......@@ -332,14 +332,14 @@ class Upload extends React.Component {
</Typography>
</TableCell>
<TableCell>
<CalcLinks calcId={archive_id} disabled={status !== 'SUCCESS'} />
<CalcLinks uploadHash={upload_hash} calcHash={calc_hash} disabled={status !== 'SUCCESS'} />
</TableCell>
</TableRow>
)
if (status === 'FAILURE') {
return (
<Tooltip key={archive_id} title={errors.map((error, index) => (<p key={`${archive_id}-${index}`}>{error}</p>))}>
<Tooltip key={calc_hash} title={errors.map((error, index) => (<p key={`${calc_hash}-${index}`}>{error}</p>))}>
{row}
</Tooltip>
)
......@@ -392,6 +392,7 @@ class Upload extends React.Component {
<TableCell colSpan={6} />
</TableRow>
)}
<TableRow>
<TablePagination
count={total}
......@@ -414,7 +415,8 @@ class Upload extends React.Component {
onClose={() => this.setState({archiveLogs: null})}
anchorEl={window.parent.document.documentElement.firstElementChild}
raiseError={this.props.raiseError}
archiveId={this.state.archiveLogs}
uploadHash={this.state.archiveLogs.uploadHash}
calcHash={this.state.archiveLogs.calcHash}
/>
)
} else {
......
......@@ -28,7 +28,7 @@ ns = api.namespace('admin', description='Administrative operations')
@api.doc(params={'operation': 'The operation to perform.'})
class AdminOperationsResource(Resource):
# TODO in production this requires authorization
@api.doc('exec')
@api.doc('exec_admin_command')
@api.response(200, 'Operation performed')
@api.response(404, 'Operation does not exist')
@api.response(400, 'Operation not available/disabled')
......
......@@ -22,6 +22,8 @@ import os.path
from flask import send_file
from flask_restplus import abort, Resource
import nomad_meta_info
from nomad import config
from nomad.files import ArchiveFile, ArchiveLogFile
from nomad.utils import get_logger
......@@ -37,9 +39,9 @@ ns = api.namespace(
@calc_route(ns, '/logs')
class ArchiveCalcLogResource(Resource):
@api.doc('get_logs')
@api.doc('get_archive_logs')
@api.response(404, 'The upload or calculation does not exist')
@api.response(200, 'Archive data send')
@api.response(200, 'Archive data send', headers={'Content-Type': 'application/plain'})
@login_if_available
def get(self, upload_hash, calc_hash):
"""
......@@ -58,7 +60,7 @@ class ArchiveCalcLogResource(Resource):
rv = send_file(
archive_path,
mimetype='application/text',
mimetype='text/plain',
as_attachment=True,
attachment_filename=os.path.basename(archive_path))
......@@ -75,7 +77,7 @@ class ArchiveCalcLogResource(Resource):
@calc_route(ns)
class ArchiveCalcResource(Resource):
@api.doc('get_calc')
@api.doc('get_archive_calc')
@api.response(404, 'The upload or calculation does not exist')
@api.response(200, 'Archive data send')
@login_if_available
......@@ -112,3 +114,33 @@ class ArchiveCalcResource(Resource):
upload_hash=upload_hash, calc_hash=calc_hash)
logger.error('Exception on accessing archive', exc_info=e)
abort(500, message='Could not accessing the archive.')
@ns.route('/metainfo/<string:metainfo_path>')
@api.doc(params=dict(metainfo_path='A path or metainfo definition file name.'))
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):
"""
Get a metainfo definition file.
"""
try:
file_dir = os.path.dirname(os.path.abspath(nomad_meta_info.__file__))
meta_info_path = os.path.normpath(os.path.join(file_dir, metainfo_path.strip()))
rv = send_file(
meta_info_path,
mimetype='application/json',
as_attachment=True,
attachment_filename=os.path.basename(metainfo_path))
return rv
except FileNotFoundError:
abort(404, message='The metainfo %s does not exist.' % metainfo_path)
except Exception as e:
logger = get_logger(
__name__, endpoint='metainfo', action='get', metainfo_path=metainfo_path)
logger.error('Exception on accessing metainfo', exc_info=e)
abort(500, message='Could not accessing the metainfo.')
......@@ -38,6 +38,8 @@ pagination_request_parser.add_argument(
'per_page', type=int, help='Desired calcs per page.', location='args')
pagination_request_parser.add_argument(
'order_by', type=str, help='The field to sort by.', location='args')
pagination_request_parser.add_argument(
'order', type=int, help='Use -1 for decending and 1 for acending order.', location='args')
def calc_route(ns, prefix: str = ''):
......
......@@ -34,7 +34,7 @@ ns = api.namespace('repo', description='Access repository metadata, edit user me
class RepoCalcResource(Resource):
@api.response(404, 'The upload or calculation does not exist')
@api.response(200, 'Metadata send')
@api.doc('get_calc')
@api.doc('get_repo_calc')
def get(self, upload_hash, calc_hash):
"""
Get calculation metadata in repository form.
......
......@@ -271,7 +271,7 @@ class UploadResource(Resource):
except NotAllowedDuringProcessing:
abort(400, message='You must not delete an upload during processing.')
@api.doc('exec')
@api.doc('exec_upload_command')
@api.response(404, 'Upload does not exist or is not allowed')
@api.response(400, 'Operation is not supported')
@api.marshal_with(upload_model, skip_none=True, code=200, description='Upload unstaged successfully')
......
......@@ -74,7 +74,7 @@ def handle_common_errors(func):
return wrapper
def upload_file(file_path: str, name: str = None, offline: bool = False, unstage: bool = False, client = None):
def upload_file(file_path: str, name: str = None, offline: bool = False, unstage: bool = False, client=None):
"""
Upload a file to nomad.
......@@ -118,7 +118,7 @@ def upload_file(file_path: str, name: str = None, offline: bool = False, unstage
for error in upload.errors:
click.echo(' %s' % error)
elif unstage:
client.uploads.exec(upload_id=upload.upload_id, operation='unstage').reponse()
client.uploads.exec_upload_command(upload_id=upload.upload_id, operation='unstage').reponse()
return upload.upload_id
......@@ -300,7 +300,7 @@ def upload(path, name: str, offline: bool, unstage: bool):
@cli.command(help='Attempts to reset the nomad.')
def reset():
_cli_client().admin.exec(operation='reset').reponse()
_cli_client().admin.exec_admin_command(operation='reset').reponse()