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

Added archive code buttons to GUI.

parent e2db43e5
Pipeline #67311 failed with stages
in 13 minutes and 13 seconds
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' 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 CodeIcon from '@material-ui/icons/Code'
import ReactJson from 'react-json-view' 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 { class ApiDialogUnstyled extends React.Component {
static propTypes = { static propTypes = {
...@@ -16,49 +19,98 @@ class ApiDialogUnstyled extends React.Component { ...@@ -16,49 +19,98 @@ class ApiDialogUnstyled extends React.Component {
content: { content: {
paddingBottom: 0 paddingBottom: 0
}, },
raw: { json: {
margin: 0, padding: 0 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() { render() {
const { classes, title, data, onClose, ...dialogProps } = this.props const { classes, title, data, onClose, ...dialogProps } = this.props
const { showRaw } = this.state
return ( return (
<Dialog {...dialogProps}> <Dialog maxWidth="lg" fullWidth {...dialogProps}>
<DialogTitle>{title || 'API'}</DialogTitle> <DialogTitle>{title || 'API Code'}</DialogTitle>
<DialogContent classes={{root: classes.content}}> <DialogContent classes={{root: classes.content}}>
{showRaw <Typography>Access the archive as JSON via <i>curl</i>:</Typography>
? <code> <div className={classes.codeContainer}>
<pre className={classes.raw}> <div className={classes.code}>
{JSON.stringify(data, null, 4)} <Markdown>{`
</pre> \`\`\`
</code> : <ReactJson ${data.curl}
src={data} \`\`\`
enableClipboard={false} `}</Markdown>
collapsed={2} </div>
displayObjectSize={false} <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> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={this.handleToggleRaw}>
{showRaw ? 'show tree' : 'show raw JSON'}
</Button>
<Button onClick={onClose}> <Button onClick={onClose}>
Close Close
</Button> </Button>
...@@ -101,9 +153,11 @@ class ApiDialogButtonUnstyled extends React.Component { ...@@ -101,9 +153,11 @@ class ApiDialogButtonUnstyled extends React.Component {
return ( return (
<div className={classes.root}> <div className={classes.root}>
{component ? component({onClick: this.handleShowDialog}) : <IconButton onClick={this.handleShowDialog}> {component ? component({onClick: this.handleShowDialog}) : <Tooltip title="Show API code">
<CodeIcon /> <IconButton onClick={this.handleShowDialog}>
</IconButton> <CodeIcon />
</IconButton>
</Tooltip>
} }
<ApiDialog <ApiDialog
{...dialogProps} open={showDialog} {...dialogProps} open={showDialog}
......
...@@ -14,6 +14,7 @@ import PeriodicTable from './PeriodicTable' ...@@ -14,6 +14,7 @@ import PeriodicTable from './PeriodicTable'
import ReloadIcon from '@material-ui/icons/Cached' import ReloadIcon from '@material-ui/icons/Cached'
import UploadList from './UploadsList' import UploadList from './UploadsList'
import GroupList from './GroupList' import GroupList from './GroupList'
import ApiDialogButton from '../ApiDialogButton'
class Search extends React.Component { class Search extends React.Component {
static tabs = { static tabs = {
...@@ -451,7 +452,12 @@ class SearchEntryList extends React.Component { ...@@ -451,7 +452,12 @@ class SearchEntryList extends React.Component {
editable={query.owner === 'staging' || query.owner === 'user'} editable={query.owner === 'staging' || query.owner === 'user'}
data={response} data={response}
onChange={setRequest} onChange={setRequest}
actions={<ReRunSearchButton/>} actions={
<React.Fragment>
<ReRunSearchButton/>
<ApiDialogButton data={response} />
</React.Fragment>
}
{...request} {...request}
{...this.props} {...this.props}
/> />
......
...@@ -20,7 +20,7 @@ The archive API of the nomad@FAIRDI APIs. This API is about serving processed ...@@ -20,7 +20,7 @@ The archive API of the nomad@FAIRDI APIs. This API is about serving processed
from typing import Dict, Any from typing import Dict, Any
from io import BytesIO from io import BytesIO
import os.path import os.path
from flask import send_file from flask import send_file, request
from flask_restplus import abort, Resource, fields from flask_restplus import abort, Resource, fields
import json import json
import importlib import importlib
...@@ -276,9 +276,8 @@ class ArchiveQueryResource(Resource): ...@@ -276,9 +276,8 @@ class ArchiveQueryResource(Resource):
abort(400, str(e)) abort(400, str(e))
# build python code and curl snippet # build python code and curl snippet
uri = os.path.join(api.base_url, ns.name, 'query') results['python'] = query_api_python('archive', 'query', query_string=request.args)
results['python'] = query_api_python(args, uri) results['curl'] = query_api_curl('archive', 'query', query_string=request.args)
results['curl'] = query_api_curl(args, uri)
data = [] data = []
calcs = results['results'] calcs = results['results']
......
...@@ -19,11 +19,12 @@ from typing import Callable, IO, Set, Tuple, Iterable, Dict, Any ...@@ -19,11 +19,12 @@ from typing import Callable, IO, Set, Tuple, Iterable, Dict, Any
from flask_restplus import fields from flask_restplus import fields
import zipstream import zipstream
from flask import stream_with_context, Response, g, abort from flask import stream_with_context, Response, g, abort
from urllib.parse import urlencode
import sys import sys
import os.path import os.path
from nomad import search from nomad import search, config
from nomad.app.optimade import filterparser from nomad.app.optimade import filterparser
from nomad.app.utils import RFC3339DateTime, rfc3339DateTime from nomad.app.utils import RFC3339DateTime, rfc3339DateTime
from nomad.files import Restricted from nomad.files import Restricted
...@@ -245,59 +246,34 @@ def streamed_zipfile( ...@@ -245,59 +246,34 @@ def streamed_zipfile(
return response return response
def resolve_query_api_url(args: Dict[str, Any], base_url: str): def query_api_url(*args, query_string: Dict[str, Any] = None):
""" """
Generates a uri from query parameters and base url. Creates a API URL.
Arguments:
*args: URL path segments after the API base URL
query_string: A dict with query string parameters
""" """
args_keys = list(args.keys()) url = os.path.join(config.api_url(False), *args)
args_keys.sort() if query_string is not None:
if args_keys == ['calc_id', 'upload_id']: url = '%s?%s' % (url, urlencode(query_string))
url = '"%s"' % os.path.join(base_url, args['upload_id'], args['calc_id'])
else:
url = '"%s?%s" % (base_url, urlencode(args))'
return url return url
def query_api_python(args: Dict[str, Any], base_url: str): def query_api_python(*args, **kwargs):
""" """
Creates a string of python code to execute a search query to the repository using Creates a string of python code to execute a search query to the repository using
the requests library. the requests library.
Arguments:
args: A dict of search parameters that will be encoded in the uri
base_url: The resource url which is prepended to the uri
""" """
str_code = 'import requests\n' url = query_api_url(*args, **kwargs)
str_code += 'from urllib.parse import urlencode\n' return '''import requests
str_code += '\n\n' response = requests.get("{}")
str_code += 'def query_repository(args, base_url):\n' response.json()'''.format(url)
str_code += ' url = %s\n' % resolve_query_api_url(args, base_url)
str_code += ' response = requests.get(url)\n'
str_code += ' if response.status_code != 200:\n' def query_api_curl(*args, **kwargs):
str_code += ' raise Exception("nomad return status %d" % response.status_code)\n'
str_code += ' return response.json()\n'
str_code += '\n\n'
str_code += 'args = {'
for key, val in args.items():
if val is None:
continue
if isinstance(val, str):
str_code += '"%s": "%s", ' % (key, val)
else:
str_code += '"%s": %s, ' % (key, val)
str_code += '}\n'
str_code += 'base_url = "%s"\n' % base_url
str_code += 'JSON_DATA = query_repository(args, base_url)\n'
return str_code
def query_api_curl(args: Dict[str, Any], base_url: str):
""" """
Creates a string of curl command to execute a search query to the repository. Creates a string of curl command to execute a search query to the repository.
Arguments:
args: A dict of search parameters that will be encoded in the uri
base_url: The resource url which is prepended to the uri
""" """
args = {key: val for key, val in args.items() if val is not None} url = query_api_url(*args, **kwargs)
uri = resolve_query_api_url(args, base_url) return 'curl -X GET %s -H "accept: application/json" --output "nomad.json"' % url
return 'curl -X GET %s -H "accept: application/json" --output "nomad.json"' % uri
...@@ -66,9 +66,8 @@ class RepoCalcResource(Resource): ...@@ -66,9 +66,8 @@ class RepoCalcResource(Resource):
abort(401, message='Not authorized to access %s/%s.' % (upload_id, calc_id)) abort(401, message='Not authorized to access %s/%s.' % (upload_id, calc_id))
result = calc.to_dict() result = calc.to_dict()
uri = os.path.join(api.base_url, ns.name, '') result['python'] = query_api_python('archive', upload_id, calc_id)
result['python'] = query_api_python({'upload_id': upload_id, 'calc_id': calc_id}, uri) result['curl'] = query_api_curl('archive', upload_id, calc_id)
result['curl'] = query_api_curl({'upload_id': upload_id, 'calc_id': calc_id}, uri)
return result, 200 return result, 200
...@@ -231,9 +230,11 @@ class RepoCalcsResource(Resource): ...@@ -231,9 +230,11 @@ class RepoCalcsResource(Resource):
results[group_name] = quantities[group_quantity] results[group_name] = quantities[group_quantity]
# build python code/curl snippet # build python code/curl snippet
uri = os.path.join(api.base_url, ns.name, '') code_args = dict(request.args)
results['curl'] = query_api_curl(args, uri) if 'statistics' in code_args:
results['python'] = query_api_python(args, uri) del(code_args['statistics'])
results['curl'] = query_api_curl('archive', 'query', query_string=code_args)
results['python'] = query_api_python('archive', 'query', query_string=code_args)
return results, 200 return results, 200
except search.ScrollIdNotFound: except search.ScrollIdNotFound:
......
Markdown is supported
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