Commit 06cbcadb authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Added uploads list to userdata page.

parent b27e6a7b
Pipeline #63619 passed with stages
in 16 minutes and 47 seconds
...@@ -32,7 +32,7 @@ class DatasetPage extends React.Component { ...@@ -32,7 +32,7 @@ class DatasetPage extends React.Component {
header: { header: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3
}, },
actions: {} actions: {}
}) })
...@@ -52,7 +52,8 @@ class DatasetPage extends React.Component { ...@@ -52,7 +52,8 @@ class DatasetPage extends React.Component {
api.search({ api.search({
owner: 'all', owner: 'all',
dataset_id: datasetId, dataset_id: datasetId,
page: 1, per_page: 1 page: 1,
per_page: 1
}).then(data => { }).then(data => {
const entry = data.results[0] const entry = data.results[0]
const dataset = entry && entry.datasets.find(ds => ds.id + '' === datasetId) const dataset = entry && entry.datasets.find(ds => ds.id + '' === datasetId)
...@@ -64,8 +65,8 @@ class DatasetPage extends React.Component { ...@@ -64,8 +65,8 @@ class DatasetPage extends React.Component {
...dataset, example: entry ...dataset, example: entry
}}) }})
}).catch(error => { }).catch(error => {
this.setState({dataset: {}}) this.setState({dataset: {}})
raiseError(error) raiseError(error)
}) })
} }
...@@ -109,7 +110,10 @@ class DatasetPage extends React.Component { ...@@ -109,7 +110,10 @@ class DatasetPage extends React.Component {
</div> </div>
</div> </div>
<SearchContext query={{dataset_id: datasetId}} ownerTypes={['all', 'public']} update={update}> <SearchContext
query={{dataset_id: datasetId}} ownerTypes={['all', 'public']} update={update}
initialRequest={{datasets: true}}
>
<Search resultTab="entries"/> <Search resultTab="entries"/>
</SearchContext> </SearchContext>
</div> </div>
......
...@@ -21,7 +21,10 @@ class UserdataPage extends React.Component { ...@@ -21,7 +21,10 @@ class UserdataPage extends React.Component {
render() { render() {
return ( return (
<div> <div>
<SearchContext ownerTypes={['user', 'staging']} initialQuery={{owner: 'user'}} > <SearchContext
ownerTypes={['user', 'staging']} initialQuery={{owner: 'user'}}
initialRequest={{uploads: true, datasets: true}}
>
<Search resultTab="entries"/> <Search resultTab="entries"/>
</SearchContext> </SearchContext>
</div> </div>
......
...@@ -18,6 +18,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard' ...@@ -18,6 +18,7 @@ import { CopyToClipboard } from 'react-copy-to-clipboard'
class DOIUnstyled extends React.Component { class DOIUnstyled extends React.Component {
static propTypes = { static propTypes = {
classes: PropTypes.object.isRequired,
doi: PropTypes.string.isRequired doi: PropTypes.string.isRequired
} }
......
...@@ -11,6 +11,7 @@ import { withDomain } from '../domains' ...@@ -11,6 +11,7 @@ import { withDomain } from '../domains'
import KeepState from '../KeepState' import KeepState from '../KeepState'
import PeriodicTable from './PeriodicTable' import PeriodicTable from './PeriodicTable'
import ReloadIcon from '@material-ui/icons/Cached' import ReloadIcon from '@material-ui/icons/Cached'
import UploadList from './UploadsList'
class Search extends React.Component { class Search extends React.Component {
static propTypes = { static propTypes = {
...@@ -41,7 +42,7 @@ class Search extends React.Component { ...@@ -41,7 +42,7 @@ class Search extends React.Component {
maxWidth: 900, maxWidth: 900,
margin: 'auto', margin: 'auto',
marginTop: theme.spacing.unit * 2, marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2, marginBottom: theme.spacing.unit * 2
}, },
searchResults: { searchResults: {
marginTop: theme.spacing.unit * 4 marginTop: theme.spacing.unit * 4
...@@ -59,6 +60,8 @@ class Search extends React.Component { ...@@ -59,6 +60,8 @@ class Search extends React.Component {
} }
} }
static contextType = SearchContext.type
state = { state = {
resultTab: this.resultTab || 'entries', resultTab: this.resultTab || 'entries',
openVisualization: this.props.visualization openVisualization: this.props.visualization
...@@ -81,6 +84,7 @@ class Search extends React.Component { ...@@ -81,6 +84,7 @@ class Search extends React.Component {
render() { render() {
const {classes} = this.props const {classes} = this.props
const {resultTab, openVisualization} = this.state const {resultTab, openVisualization} = this.state
const {state: {request: {uploads, datasets}}} = this.context
return <DisableOnLoading> return <DisableOnLoading>
<div className={classes.root}> <div className={classes.root}>
...@@ -104,9 +108,9 @@ class Search extends React.Component { ...@@ -104,9 +108,9 @@ class Search extends React.Component {
<div className={classes.visalizations}> <div className={classes.visalizations}>
{Object.keys(Search.visalizations).map(key => { {Object.keys(Search.visalizations).map(key => {
return Search.visalizations[key].render({ return Search.visalizations[key].render({
key: key, open: openVisualization === key key: key, open: openVisualization === key
})
}) })
})
} }
</div> </div>
...@@ -119,17 +123,22 @@ class Search extends React.Component { ...@@ -119,17 +123,22 @@ class Search extends React.Component {
onChange={(event, value) => this.setState({resultTab: value})} onChange={(event, value) => this.setState({resultTab: value})}
> >
<Tab label="Entries" value="entries" /> <Tab label="Entries" value="entries" />
<Tab label="Datasets" value="datasets" /> {datasets && <Tab label="Datasets" value="datasets" />}
{uploads && <Tab label="Uploads" value="uploads" />}
</Tabs> </Tabs>
<KeepState <KeepState
visible={resultTab === 'entries'} visible={resultTab === 'entries'}
render={() => <SearchEntryList />} render={() => <SearchEntryList />}
/> />
<KeepState {datasets && <KeepState
visible={resultTab === 'datasets'} visible={resultTab === 'datasets'}
render={() => <SearchDatasetList />} render={() => <SearchDatasetList />}
/> />}
{uploads && <KeepState
visible={resultTab === 'uploads'}
render={() => <SearchUploadList />}
/>}
</Paper> </Paper>
</div> </div>
</div> </div>
...@@ -212,7 +221,6 @@ class ElementsVisualization extends React.Component { ...@@ -212,7 +221,6 @@ class ElementsVisualization extends React.Component {
} }
class MetricSelectUnstyled extends React.Component { class MetricSelectUnstyled extends React.Component {
static propTypes = { static propTypes = {
classes: PropTypes.object.isRequired, classes: PropTypes.object.isRequired,
domain: PropTypes.object.isRequired domain: PropTypes.object.isRequired
...@@ -394,7 +402,7 @@ class SearchEntryList extends React.Component { ...@@ -394,7 +402,7 @@ class SearchEntryList extends React.Component {
render() { render() {
const {state: {response, request, query}, props, setRequest} = this.context const {state: {response, request, query}, props, setRequest} = this.context
return <EntryList return <EntryList
query={{...query, ...props.query}} query={{...query, ...props.query}}
editable={query.owner === 'staging' || query.owner === 'user'} editable={query.owner === 'staging' || query.owner === 'user'}
data={response} data={response}
...@@ -420,4 +428,19 @@ class SearchDatasetList extends React.Component { ...@@ -420,4 +428,19 @@ class SearchDatasetList extends React.Component {
} }
} }
class SearchUploadList extends React.Component {
static contextType = SearchContext.type
render() {
const {state: {response}, setRequest} = this.context
return <UploadList data={response}
total={response.statistics.total.all.uploads}
onChange={setRequest}
actions={<ReRunSearchButton/>}
{...response}
/>
}
}
export default withStyles(Search.styles)(Search) export default withStyles(Search.styles)(Search)
...@@ -12,7 +12,12 @@ class SearchContext extends React.Component { ...@@ -12,7 +12,12 @@ class SearchContext extends React.Component {
static propTypes = { static propTypes = {
query: PropTypes.object, query: PropTypes.object,
initialQuery: PropTypes.object, initialQuery: PropTypes.object,
update: PropTypes.number initialRequest: PropTypes.object,
update: PropTypes.number,
domain: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
children: PropTypes.any
} }
static emptyResponse = { static emptyResponse = {
...@@ -24,6 +29,10 @@ class SearchContext extends React.Component { ...@@ -24,6 +29,10 @@ class SearchContext extends React.Component {
after: null, after: null,
values: [] values: []
}, },
uploads: {
after: null,
values: []
},
statistics: { statistics: {
total: { total: {
all: { all: {
...@@ -41,6 +50,9 @@ class SearchContext extends React.Component { ...@@ -41,6 +50,9 @@ class SearchContext extends React.Component {
this.handleQueryChange = this.handleQueryChange.bind(this) this.handleQueryChange = this.handleQueryChange.bind(this)
this.handleMetricChange = this.handleMetricChange.bind(this) this.handleMetricChange = this.handleMetricChange.bind(this)
this.state.query = this.props.initialQuery || {} this.state.query = this.props.initialQuery || {}
if (this.props.initialRequest) {
this.state.request = {...this.state.request, ...this.props.initialRequest}
}
} }
defaultMetric = this.props.domain.defaultSearchMetric defaultMetric = this.props.domain.defaultSearchMetric
...@@ -52,9 +64,7 @@ class SearchContext extends React.Component { ...@@ -52,9 +64,7 @@ class SearchContext extends React.Component {
order_by: 'formula', order_by: 'formula',
order: 1, order: 1,
page: 1, page: 1,
per_page: 10, per_page: 10
datasets: true,
datasets_after: null
}, },
metric: this.defaultMetric, metric: this.defaultMetric,
usedMetric: this.defaultMetric, usedMetric: this.defaultMetric,
...@@ -104,7 +114,7 @@ class SearchContext extends React.Component { ...@@ -104,7 +114,7 @@ class SearchContext extends React.Component {
this.setState({response: response || SearchContext.emptyResponse, usedMetric: usedMetric}) this.setState({response: response || SearchContext.emptyResponse, usedMetric: usedMetric})
}).catch(error => { }).catch(error => {
this.setState({response: SearchContext.emptyResponse}) this.setState({response: SearchContext.emptyResponse})
if (error.name !== 'NotAuthorized' || this.props.searchParameters.owner === 'all') { if (error.name !== 'NotAuthorized') {
raiseError(error) raiseError(error)
} }
}) })
...@@ -117,7 +127,7 @@ class SearchContext extends React.Component { ...@@ -117,7 +127,7 @@ class SearchContext extends React.Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const {query, request, metric} = this.state const {query, request, metric} = this.state
if ( if (
prevState.query !== query || prevState.query !== query ||
prevState.request !== request || prevState.request !== request ||
prevState.metric !== metric || prevState.metric !== metric ||
prevProps.update !== this.props.update || prevProps.update !== this.props.update ||
......
...@@ -83,7 +83,7 @@ class SearchPage extends React.Component { ...@@ -83,7 +83,7 @@ class SearchPage extends React.Component {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<SearchContext <SearchContext
initialQuery={query} initialQuery={query} initialRequest={{datasets: true}}
ownerTypes={['all', 'public'].filter(key => user || withoutLogin.indexOf(key) !== -1)} ownerTypes={['all', 'public'].filter(key => user || withoutLogin.indexOf(key) !== -1)}
> >
<Search visualization="elements" /> <Search visualization="elements" />
......
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, TableCell, Toolbar, IconButton, FormGroup, Tooltip } from '@material-ui/core'
import { compose } from 'recompose'
import { withRouter } from 'react-router'
import { withDomain } from '../domains'
import NextIcon from '@material-ui/icons/ChevronRight'
import StartIcon from '@material-ui/icons/SkipPrevious'
import DataTable from '../DataTable'
import { withApi } from '../api'
import EditUserMetadataDialog from '../EditUserMetadataDialog'
import DownloadButton from '../DownloadButton'
import ClipboardIcon from '@material-ui/icons/Assignment'
import { CopyToClipboard } from 'react-copy-to-clipboard'
class UploadIdUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
uploadId: PropTypes.string.isRequired
}
static styles = theme => ({
root: {
display: 'inline-flex',
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'nowrap'
}
})
render() {
const {classes, uploadId} = this.props
return <span className={classes.root}>
{uploadId}
<CopyToClipboard
text={uploadId} onCopy={() => null}
>
<Tooltip title={`Copy to clipboard`}>
<IconButton style={{margin: 3, marginRight: 0, padding: 4}}>
<ClipboardIcon style={{fontSize: 16}} />
</IconButton>
</Tooltip>
</CopyToClipboard>
</span>
}
}
export const UploadId = withStyles(UploadIdUnstyled.styles)(UploadIdUnstyled)
class UploadActionsUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
upload: PropTypes.object.isRequired,
user: PropTypes.object,
onChange: PropTypes.func
}
static styles = theme => ({
group: {
flexWrap: 'nowrap',
flexDirection: 'row-reverse'
}
})
constructor(props) {
super(props)
this.handleEdit = this.handleEdit.bind(this)
}
handleEdit() {
const {onChange, upload} = this.props
if (onChange) {
onChange(upload)
}
}
render() {
const {upload, user, classes} = this.props
const editable = user && upload.example &&
upload.example.authors.find(author => author.user_id === user.sub)
const query = {upload_id: upload.example.upload_id}
return <FormGroup row classes={{root: classes.group}}>
{<DownloadButton query={query} tooltip="Download upload" />}
{editable && <EditUserMetadataDialog
title="Edit metadata of all entries in this upload"
example={upload.example} query={query}
total={upload.total} onEditComplete={this.handleEdit}
/>}
</FormGroup>
}
}
export const UploadActions = compose(withApi(false), withStyles(UploadActionsUnstyled.styles))(UploadActionsUnstyled)
class UploadListUnstyled extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
total: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
history: PropTypes.any.isRequired,
uploads_after: PropTypes.string,
actions: PropTypes.element
}
static styles = theme => ({
root: {
overflow: 'auto',
paddingLeft: theme.spacing.unit * 2,
paddingRight: theme.spacing.unit * 2
},
scrollCell: {
padding: 0
},
scrollBar: {
minHeight: 56,
padding: 0
},
scrollSpacer: {
flexGrow: 1
},
clickableRow: {
cursor: 'pointer'
}
})
constructor(props) {
super(props)
this.renderEntryActions = this.renderEntryActions.bind(this)
}
columns = {
upload_time: {
label: 'Upload time',
render: (upload) => new Date(upload.example.upload_time).toLocaleString()
},
upload_id: {
label: 'Id',
render: (upload) => <UploadId uploadId={upload.example.upload_id} />
},
last_processing: {
label: 'Last processed',
render: (upload) => new Date(upload.example.last_processing).toLocaleString()
},
version: {
label: 'Processed with version',
render: (upload) => upload.example.nomad_version
},
entries: {
label: 'Entries',
render: (upload) => upload.total
},
published: {
label: 'Published',
render: (upload) => upload.example.published ? 'Yes' : 'No'
}
}
renderEntryActions(entry) {
const {onChange} = this.props
return <UploadActions search upload={entry} onChange={() => onChange({})} />
}
render() {
const { classes, data, total, uploads_after, onChange, actions } = this.props
const uploads = data.uploads || {values: []}
const results = Object.keys(uploads.values).map(id => {
return {
id: id,
total: uploads.values[id].total,
example: uploads.values[id].examples[0]
}
})
const per_page = 10
const after = uploads.after
let paginationText
if (uploads_after) {
paginationText = `next ${results.length} of ${total}`
} else {
paginationText = `1-${results.length} of ${total}`
}
const pagination = <TableCell colSpan={1000} classes={{root: classes.scrollCell}}>
<Toolbar className={classes.scrollBar}>
<span className={classes.scrollSpacer}>&nbsp;</span>
<span>{paginationText}</span>
<IconButton disabled={!uploads_after} onClick={() => onChange({uploads_after: null})}>
<StartIcon />
</IconButton>
<IconButton disabled={results.length < per_page} onClick={() => onChange({uploads_after: after})}>
<NextIcon />
</IconButton>
</Toolbar>
</TableCell>
return <DataTable
title={`${total.toLocaleString()} uploads`}
id={row => row.id}
total={total}
columns={this.columns}
selectedColumns={['upload_time', 'upload_name', 'upload_id', 'entries', 'published']}
entryActions={this.renderEntryActions}
data={results}
rows={per_page}
actions={actions}
pagination={pagination}
/>
}
}
const UploadList = compose(withRouter, withDomain, withApi(false), withStyles(UploadListUnstyled.styles))(UploadListUnstyled)
export default UploadList
...@@ -80,7 +80,11 @@ repo_calcs_model = api.model('RepoCalculations', { ...@@ -80,7 +80,11 @@ repo_calcs_model = api.model('RepoCalculations', {
' metrics over all results. ' % ', '.join(datamodel.Domain.instance.metrics_names))), ' metrics over all results. ' % ', '.join(datamodel.Domain.instance.metrics_names))),
'datasets': fields.Nested(api.model('RepoDatasets', { 'datasets': fields.Nested(api.model('RepoDatasets', {
'after': fields.String(description='The after value that can be used to retrieve the next datasets.'), 'after': fields.String(description='The after value that can be used to retrieve the next datasets.'),
'values': fields.Raw(description='A dict with names as key. The values are dicts with "total" and "examples" keys.') 'values': fields.Raw(description='A dict with dataset id as key. The values are dicts with "total" and "examples" keys.')
}), skip_none=True),
'uploads': fields.Nested(api.model('RepoUploads', {
'after': fields.String(description='The after value that can be used to retrieve the next uploads.'),
'values': fields.Raw(description='A dict with upload ids as key. The values are dicts with "total" and "examples" keys.')
}), skip_none=True) }), skip_none=True)
}) })
...@@ -117,12 +121,16 @@ repo_request_parser.add_argument( ...@@ -117,12 +121,16 @@ repo_request_parser.add_argument(
'date_histogram', type=bool, help='Add an additional aggregation over the upload time') 'date_histogram', type=bool, help='Add an additional aggregation over the upload time')
repo_request_parser.add_argument( repo_request_parser.add_argument(
'datasets_after', type=str, help='The last dataset id of the last scroll window for the dataset quantity') 'datasets_after', type=str, help='The last dataset id of the last scroll window for the dataset quantity')
repo_request_parser.add_argument(