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

Merge branch 'GUI_Download' into 'v1.0.0'

Fix Downloading

See merge request !435
parents 3d3b6780 8a9a5b92
Pipeline #114137 passed with stages
in 27 minutes and 15 seconds
......@@ -23,14 +23,9 @@ import { apiBase } from '../../config'
import { Tooltip, IconButton, Menu, MenuItem } from '@material-ui/core'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import { useApi } from '../api'
import qs from 'qs'
import { searchToQsData } from '../search/SearchContext'
import { toAPIFilter } from '../search/SearchContext'
function stringify(query) {
return qs.stringify(query, {indices: false, encode: false})
}
const DownloadButton = React.memo(function DownloadButton(props) {
const EntryDownloadButton = React.memo(function EntryDownloadButton(props) {
const {tooltip, disabled, buttonProps, dark, query} = props
const {api, user} = useApi()
const {raiseError} = useErrors()
......@@ -38,16 +33,11 @@ const DownloadButton = React.memo(function DownloadButton(props) {
const [preparingDownload, setPreparingDownload] = useState(false)
const [anchorEl, setAnchorEl] = useState(null)
const handleClick = event => {
event.stopPropagation()
setAnchorEl(event.currentTarget)
}
const handleSelect = (choice) => {
setAnchorEl(null)
let queryStringData = searchToQsData({query})
const openDownload = () => {
const url = `${apiBase}/v1/entries/${choice}/download?${stringify(queryStringData)}`
const download = (choice) => {
let queryStringData = toAPIFilter(query)
const owner = query.visibility || 'visible'
const openDownload = (token) => {
const url = `${apiBase}/v1/entries/${choice}/download?owner=${owner}&signature_token=${token}&json_query=${JSON.stringify(queryStringData)}`
FileSaver.saveAs(url, `nomad-${choice}-download.zip`)
}
......@@ -55,8 +45,8 @@ const DownloadButton = React.memo(function DownloadButton(props) {
setPreparingDownload(true)
api.get('/auth/signature_token')
.then(response => {
queryStringData.signature_token = response.signature_token
openDownload()
const token = response.signature_token
openDownload(token)
})
.catch(raiseError)
.finally(setPreparingDownload(false))
......@@ -65,6 +55,16 @@ const DownloadButton = React.memo(function DownloadButton(props) {
}
}
const handleClick = event => {
event.stopPropagation()
setAnchorEl(event.currentTarget)
}
const handleSelect = (choice) => {
setAnchorEl(null)
download(choice)
}
const handleClose = () => {
setAnchorEl(null)
}
......@@ -90,7 +90,7 @@ const DownloadButton = React.memo(function DownloadButton(props) {
</Menu>
</React.Fragment>
})
DownloadButton.propTypes = {
EntryDownloadButton.propTypes = {
/**
* The query that defines what to download.
*/
......@@ -110,4 +110,4 @@ DownloadButton.propTypes = {
dark: PropTypes.bool
}
export default DownloadButton
export default EntryDownloadButton
/*
* Copyright The NOMAD Authors.
*
* This file is part of NOMAD. See https://nomad-lab.eu for further info.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import FileSaver from 'file-saver'
import { useErrors } from '../errors'
import { apiBase } from '../../config'
import { Tooltip, IconButton } from '@material-ui/core'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import { useApi } from '../api'
import { toAPIFilter } from '../search/SearchContext'
const UploadDownloadButton = React.memo(function UploadDownloadButton(props) {
const {tooltip, disabled, buttonProps, dark, query} = props
const {api, user} = useApi()
const {raiseError} = useErrors()
const [preparingDownload, setPreparingDownload] = useState(false)
const download = () => {
let queryStringData = toAPIFilter(query)
const owner = query.visibility || 'visible'
const openDownload = (token) => {
const url = `${apiBase}/v1/uploads/${query.upload_id}/raw?offset=0&length=-1&compress=true&owner=${owner}&signature_token=${token}&json_query=${JSON.stringify(queryStringData)}`
FileSaver.saveAs(url, `nomad-download.zip`)
}
if (user) {
setPreparingDownload(true)
api.get('/auth/signature_token')
.then(response => {
const token = response.signature_token
openDownload(token)
})
.catch(raiseError)
.finally(setPreparingDownload(false))
} else {
openDownload()
}
}
const handleClick = event => {
event.stopPropagation()
download()
}
return <React.Fragment>
<IconButton
{...buttonProps}
disabled={disabled || preparingDownload}
onClick={handleClick}
style={dark ? {color: 'white'} : null}
>
<Tooltip title={tooltip || 'Download'}>
<DownloadIcon />
</Tooltip>
</IconButton>
</React.Fragment>
})
UploadDownloadButton.propTypes = {
/**
* The query that defines what to download.
*/
query: PropTypes.object.isRequired,
/**
* A tooltip for the button
*/
tooltip: PropTypes.string,
/**
* Whether the button is disabled
*/
disabled: PropTypes.bool,
/**
* Properties forwarded to the button.
*/
buttonProps: PropTypes.object,
dark: PropTypes.bool
}
export default UploadDownloadButton
......@@ -18,7 +18,7 @@
import React, { useState, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Link } from '@material-ui/core'
import DownloadButton from '../../entry/DownloadButton'
import EntryDownloadButton from '../../entry/EntryDownloadButton'
import EntryDetails, { EntryRowActions, Published } from '../../entry/EntryDetails'
import { authorList } from '../../../utils'
import {
......@@ -98,7 +98,7 @@ const SearchResultsEntries = React.memo(function SearchResultsEntries(props) {
return searchQuery
}
return {owner: 'visible', entry_id: selected.map(data => data.entry_id)}
return {entry_id: selected.map(data => data.entry_id)}
}, [selected, searchQuery])
return <Datatable
......@@ -107,7 +107,7 @@ const SearchResultsEntries = React.memo(function SearchResultsEntries(props) {
>
<DatatableToolbar title={`${data.length}/${pagination.total} search results`}>
<DatatableToolbarActions selection>
<DownloadButton tooltip="Download files" query={query} />
<EntryDownloadButton tooltip="Download files" query={query} />
</DatatableToolbarActions>
</DatatableToolbar>
<DatatableTable actions={EntryRowActions} details={EntryDetails}>
......
......@@ -24,7 +24,7 @@ import {
addColumnDefaults,
Datatable, DatatablePagePagination, DatatableTable,
DatatableToolbar, DatatableToolbarActions } from '../datatable/Datatable'
import DownloadButton from '../entry/DownloadButton'
import EntryDownloadButton from '../entry/EntryDownloadButton'
import EditUserMetadataDialog from '../entry/EditUserMetadataDialog'
const columns = [
......@@ -81,10 +81,10 @@ export default function ProcessingTable(props) {
const selectedQuery = useMemo(() => {
if (selected === 'all') {
return {owner: 'visible', 'upload_id': upload.upload_id}
return {'upload_id': upload.upload_id}
}
return {owner: 'visible', entry_id: selected.map(data => data.entry_id)}
return {entry_id: selected.map(data => data.entry_id)}
}, [selected, upload])
return <Paper>
......@@ -94,7 +94,7 @@ export default function ProcessingTable(props) {
>
<DatatableToolbar title={`${pagination.total} search results`}>
<DatatableToolbarActions selection>
<DownloadButton tooltip="Download files" query={selectedQuery} />
<EntryDownloadButton tooltip="Download files" query={selectedQuery} />
{!upload.published && <EditUserMetadataDialog
example={selected === 'all' ? data[0] : selected[0]}
query={selectedQuery}
......
......@@ -42,6 +42,7 @@ import EditUserMetadataDialog from '../entry/EditUserMetadataDialog'
import Page from '../Page'
import { getUrl } from '../nav/Routes'
import { combinePagination } from '../datatable/Datatable'
import UploadDownloadButton from '../entry/UploadDownloadButton'
const useDropButtonStyles = makeStyles(theme => ({
dropzone: {
......@@ -366,7 +367,6 @@ function UploadPage() {
}
const handleReprocess = () => {
setDeleteClicked(true)
api.post(`/uploads/${uploadId}/action/process`)
.then(results => setUpload(results.data))
.catch(errors.raiseError)
......@@ -406,7 +406,7 @@ function UploadPage() {
</Grid>
<Grid item style={{flexGrow: 1}}>
<Typography>Upload is processing ...</Typography>
<Typography>{data.upload.current_process}</Typography>
<Typography>{data.upload.last_status_message}</Typography>
</Grid>
</Grid>
</Page>
......@@ -428,6 +428,7 @@ function UploadPage() {
<MembersIcon />
</Tooltip>
</IconButton>
<UploadDownloadButton tooltip="Download files" query={{'upload_id': uploadId}} />
<IconButton disabled={isPublished} onClick={handleReprocess}>
<Tooltip title="Reprocess">
<ReprocessIcon />
......@@ -467,7 +468,7 @@ function UploadPage() {
disabled={isProcessing} />
</React.Fragment>
)}
<FilesBrower className={classes.stepContent} uploadId={uploadId} disabled={isProcessing} />
<FilesBrower className={classes.stepContent} uploadId={uploadId} disabled={isProcessing || deleteClicked} />
</StepContent>
</Step>
<Step expanded={!isEmpty}>
......
......@@ -34,8 +34,9 @@ import UploaderIcon from '@material-ui/icons/AccountCircle'
import DetailsIcon from '@material-ui/icons/MoreHoriz'
import { UploadButton } from '../nav/Routes'
import {
addColumnDefaults, combinePagination, Datatable, DatatableLoadMorePagination,
DatatableTable, DatatableToolbar } from '../datatable/Datatable'
addColumnDefaults, combinePagination, Datatable, DatatablePagePagination,
DatatableTable, DatatableToolbar
} from '../datatable/Datatable'
import TooltipButton from '../utils/TooltipButton'
import ReloadIcon from '@material-ui/icons/Replay'
......@@ -313,7 +314,7 @@ function UploadsPage() {
</TooltipButton>
</DatatableToolbar>
<DatatableTable actions={UploadActions}>
<DatatableLoadMorePagination />
<DatatablePagePagination />
</DatatableTable>
</Datatable>
</Paper>
......
......@@ -34,6 +34,7 @@ import datetime
import numpy as np
import re
import fnmatch
import json
from nomad import datamodel # pylint: disable=unused-import
from nomad.utils import strip
......@@ -322,6 +323,9 @@ class QueryParameters:
request: Request,
owner: Optional[Owner] = FastApiQuery(
'public', description=strip(Owner.__doc__)),
json_query: Optional[str] = FastApiQuery(None, description=strip('''
To pass a query string in the format of JSON e.g. '{{"results.material.elements": ["H", "O"]}}'.
''')),
q: Optional[List[str]] = FastApiQuery(
[], description=strip('''
Since we cannot properly offer forms for all parameters in the OpenAPI dashboard,
......@@ -409,6 +413,14 @@ class QueryParameters:
raise HTTPException(
422, detail=[{'loc': ['query', key], 'msg': 'operator %s is unknown' % op}])
# process the json_query
if json_query is not None:
try:
query.update(**json.loads(json_query))
except Exception:
raise HTTPException(
422, detail=[{'loc': ['json_query'], 'msg': 'cannot parse json_query'}])
return WithQuery(query=query, owner=owner)
......
......@@ -571,7 +571,7 @@ async def get_upload_raw_path(
Set if compressed files should be decompressed before streaming the
content (that is: if there are compressed files *within* the raw files).
Note, only some compression formats are supported.''')),
user: User = Depends(create_user_dependency(required=True))):
user: User = Depends(create_user_dependency(required=True, signature_token_auth_allowed=True))):
'''
For the upload specified by `upload_id`, gets the raw file or directory content located
at the given `path`. The data is zipped if `compress = true`.
......
......@@ -102,7 +102,9 @@ def get_query_test_parameters(
pytest.param({'q': f'{entry_prefix}upload_create_time__gt__2014-01-01'}, 200, total, id='datetime'),
pytest.param({'q': [elements + '__all__H', elements + '__all__O']}, 200, total, id='q-all'),
pytest.param({'q': [elements + '__all__H', elements + '__all__X']}, 200, 0, id='q-all'),
pytest.param({'q': f'{upload_create_time}__gt__1970-01-01'}, 200, total, id='date')
pytest.param({'q': f'{upload_create_time}__gt__1970-01-01'}, 200, total, id='date'),
pytest.param({'json_query': f'{{"{elements}": ["H", "O"]}}'}, 200, total, id='json_query'),
pytest.param({'json_query': f'{{"{elements}": ["H", "O"}}'}, 422, 0, id='invalid-json_query')
]
......
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