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

Merge branch 'v0.7.1' into 'master'

V0.7.1

See merge request !72
parents 05778e75 e5dc46b6
Pipeline #65838 passed with stage
in 44 seconds
......@@ -78,6 +78,10 @@ your browser.
## Change log
Omitted versions are plain bugfix releases with only minor changes and fixes.
### v0.7.1
- Download of archive files based on search queries
- minor bugfixes
### v0.7.0
- User metadata editing and datasets with DOIs
- Revised GUI lists (entries, grouped entries, datasets, uploads)
......
window.nomadEnv = {
'keycloakBase': 'https://labdev-nomad.esc.rzg.mpg.de/fairdi/keycloak/auth/',
'keycloakBase': 'https://repository.nomad-coe.eu/fairdi/keycloak/auth/',
'keycloakRealm': 'fairdi_nomad_test',
'keycloakClientId': 'nomad_gui_dev',
'appBase': 'http://localhost:8000/fairdi/nomad/latest',
......
......@@ -38,6 +38,10 @@ class DataTableToolbarUnStyled extends React.Component {
color: theme.palette.secondary.main
},
title: {
whiteSpace: 'nowrap',
marginRight: theme.spacing.unit
},
grow: {
flex: '1 1 100%'
}
})
......@@ -79,13 +83,53 @@ class DataTableToolbarUnStyled extends React.Component {
const { anchorEl } = this.state
const open = Boolean(anchorEl)
const regularActions = <React.Fragment>
{actions || <React.Fragment/>}
<Tooltip title="Change displayed columns">
<IconButton onClick={this.handleClick}>
<ViewColumnIcon />
</IconButton>
</Tooltip>
<Popover
open={open}
anchorEl={anchorEl}
onClose={this.handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
>
<List>
{Object.keys(columns).map(key => {
const column = columns[key]
return (
<ListItem key={key} role={undefined} dense button onClick={() => this.handleToggle(key)}>
<Checkbox
checked={selectedColumns.indexOf(key) !== -1}
tabIndex={-1}
disableRipple
/>
<ListItemText primary={column.label} />
</ListItem>
)
})}
</List>
</Popover>
</React.Fragment>
if (numSelected > 0) {
return (
<Toolbar className={clsx(classes.root, {[classes.selected]: true})} >
<Typography className={classes.title} color="inherit" variant="h6">
{numSelected.toLocaleString()} selected
{numSelected.toLocaleString()} selected:
</Typography>
{selectActions}
<span className={classes.grow} />
{regularActions}
</Toolbar>
)
} else {
......@@ -94,41 +138,8 @@ class DataTableToolbarUnStyled extends React.Component {
<Typography className={classes.title} variant="h6" id="tableTitle">
{title || ''}
</Typography>
{actions || <React.Fragment/>}
<Tooltip title="Change displayed columns">
<IconButton onClick={this.handleClick}>
<ViewColumnIcon />
</IconButton>
</Tooltip>
<Popover
open={open}
anchorEl={anchorEl}
onClose={this.handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
>
<List>
{Object.keys(columns).map(key => {
const column = columns[key]
return (
<ListItem key={key} role={undefined} dense button onClick={() => this.handleToggle(key)}>
<Checkbox
checked={selectedColumns.indexOf(key) !== -1}
tabIndex={-1}
disableRipple
/>
<ListItemText primary={column.label} />
</ListItem>
)
})}
</List>
</Popover>
<span className={classes.grow} />
{regularActions}
</Toolbar>
)
}
......
......@@ -113,7 +113,7 @@ class DatasetPage extends React.Component {
<SearchContext
query={{dataset_id: datasetId}} ownerTypes={['all', 'public']} update={update}
>
<Search resultTab="entries" groups datasets />
<Search resultTab="entries" tabs={['entries', 'groups', 'datasets']} />
</SearchContext>
</div>
)
......
......@@ -5,7 +5,7 @@ import { withApi } from './api'
import { compose } from 'recompose'
import { withErrors } from './errors'
import { apiBase } from '../config'
import { Tooltip, IconButton } from '@material-ui/core'
import { Tooltip, IconButton, Menu, MenuItem } from '@material-ui/core'
import DownloadIcon from '@material-ui/icons/CloudDownload'
class DownloadButton extends React.Component {
......@@ -14,10 +14,6 @@ class DownloadButton extends React.Component {
* The query that defines what to download.
*/
query: PropTypes.object.isRequired,
/**
* A suggestion for the download filename.
*/
fileName: PropTypes.string,
/**
* A tooltip for the button
*/
......@@ -37,17 +33,19 @@ class DownloadButton extends React.Component {
}
state = {
preparingDownload: false
preparingDownload: false,
anchorEl: null
}
async onDownloadClicked(event) {
handleClick(event) {
event.stopPropagation()
this.setState({ anchorEl: event.currentTarget });
}
const {api, query, user, fileName, raiseError} = this.props
async handleSelect(choice) {
const {api, query, user, raiseError} = this.props
const params = {
strip: true
}
const params = {}
Object.keys(query).forEach(key => { params[key] = query[key] })
if (user) {
......@@ -59,25 +57,39 @@ class DownloadButton extends React.Component {
raiseError(e)
}
}
FileSaver.saveAs(`${apiBase}/raw/query?${new URLSearchParams(params).toString()}`, fileName || 'nomad-download.zip')
this.setState({preparingDownload: false})
FileSaver.saveAs(`${apiBase}/${choice}/query?${new URLSearchParams(params).toString()}`, `nomad-${choice}-download.zip`)
this.setState({preparingDownload: false, anchorEl: null})
}
handleClose() {
this.setState({anchorEl: null})
}
render() {
const {tooltip, disabled, buttonProps, dark} = this.props
const {preparingDownload} = this.state
const {preparingDownload, anchorEl} = this.state
const props = {
...buttonProps,
disabled: disabled || preparingDownload,
onClick: this.onDownloadClicked.bind(this)
onClick: this.handleClick.bind(this)
}
return <IconButton {...props} style={dark ? {color: 'white'} : null}>
<Tooltip title={tooltip || 'Download'}>
<DownloadIcon />
</Tooltip>
</IconButton>
return <React.Fragment>
<IconButton {...props} style={dark ? {color: 'white'} : null}>
<Tooltip title={tooltip || 'Download'}>
<DownloadIcon />
</Tooltip>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={this.handleClose.bind(this)}
>
<MenuItem onClick={() => this.handleSelect('raw')}>Raw uploaded files</MenuItem>
<MenuItem onClick={() => this.handleSelect('archive')}>NOMAD Archive files</MenuItem>
</Menu>
</React.Fragment>
}
}
......
......@@ -176,6 +176,8 @@ class MyAutosuggestUnstyled extends React.PureComponent {
ref(node)
inputRef(node)
},
name: 'search', // try to prevent browsers ignore autocomplete="off"
type: 'search', // try to prevent browsers ignore autocomplete="off"
classes: {
input: classes.input
}
......@@ -395,6 +397,7 @@ class ReferenceInput extends React.Component {
return <TextField
fullWidth
{...rest}
type="search" name="search" // attempt to avoid browsers autofill, since they seem to ignore autocomplete="off"
value={this.state.inputValue}
onChange={this.handleChange.bind(this)}
error={value === undefined}
......@@ -1048,7 +1051,7 @@ class EditUserMetadataDialogUnstyled extends React.Component {
if (submitting) {
return <DialogActions>
<DialogContentText color="warning" style={{marginLeft: 16}}>Do not close the page. This might take up to several minutes for editing many entries.</DialogContentText>
<DialogContentText color="error" style={{marginLeft: 16}}>Do not close the page. This might take up to several minutes for editing many entries.</DialogContentText>
<span style={{flexGrow: 1}} />
<div className={classes.submitWrapper}>
<Button onClick={this.handleSubmit} disabled={!submitEnabled} color="primary">
......
......@@ -54,7 +54,7 @@ class FAQ extends React.Component {
*mainfiles* and *auxiliary* files can be downloaded publicly.
If you know a file to be the main output file of a supported code, and it is
still not recognized by NOMAD, let us know.
still not recognized by NOMAD, let us know: [${email}](mailto:${email}).
### Some of my data is marked as *not processed* or *unavailable*, what does this mean?
......@@ -128,7 +128,7 @@ class FAQ extends React.Component {
### I want to upload data from a code that is not yet supported?
If you are familiar with the input and output format of other relevant codes,
write us an Email ([${email}](mailto:${email})) and we will figure our if and how
write us an Email ([${email}](mailto:${email})) and we will figure out if and how
to support this code in the future.
`}</Markdown>
</div>
......
......@@ -61,11 +61,11 @@ class UserdataPage extends React.Component {
<SearchContext
{...this.props}
ownerTypes={['user', 'staging']} initialQuery={{owner: 'user'}}
initialRequest={{order_by: 'upload_time'}}
initialRequest={{order_by: 'upload_time', uploads: true}}
>
<Search
resultTab="entries"
datasets uploads
resultTab="uploads"
tabs={['uploads', 'datasets', 'entries']}
entryListProps={{selectedColumns: ['formula', 'upload_time', 'mainfile', 'co_authors', 'references', 'datasets']}}
/>
</SearchContext>
......
......@@ -48,6 +48,7 @@ class DomainProviderBase extends React.Component {
exploring capabilities (menu items on the left).
`,
entryLabel: 'entry',
entryLabelPlural: 'entries',
searchPlaceholder: 'enter atoms, codes, functionals, or other quantity values',
/**
* A component that is used to render the search aggregations. The components needs
......@@ -181,6 +182,7 @@ class DomainProviderBase extends React.Component {
methods and respective data.
`,
entryLabel: 'experiment',
entryLabelPlural: 'experiments',
searchPlaceholder: 'enter atoms, experimental methods, or other quantity values',
/**
* A component that is used to render the search aggregations. The components needs
......
......@@ -38,10 +38,12 @@ class RawFiles extends React.Component {
fileContents: {
width: '85%',
overflowX: 'auto',
color: 'white',
background: '#222',
marginTop: 16,
padding: 8
color: theme.palette.primary.contrastText,
backgroundColor: theme.palette.primary.dark,
marginTop: theme.spacing.unit,
padding: '3px 6px',
fontFamily: 'Consolas, "Liberation Mono", Menlo, Courier, monospace',
fontSize: 12
},
fileError: {
marginTop: 16,
......@@ -97,7 +99,7 @@ class RawFiles extends React.Component {
}
this.props.api.getRawFileListFromCalc(uploadId, calcId).then(data => {
const files = data.contents.map(file => `${data.directory}/${file.name}`)
const files = data.contents.map(file => data.directory ? `${data.directory}/${file.name}` : file.name)
if (files.length > 500) {
raiseError('There are more than 500 files in this entry. We can only show the first 500.')
}
......
......@@ -39,12 +39,9 @@ If you bookmark this page, you can save the definition represented by the highli
To learn more about the meta-info, visit the [meta-info homepage](https://metainfo.nomad-coe.eu/nomadmetainfo_public/archive.html).
`
const ITEM_HEIGHT = 48
const ITEM_PADDING_TOP = 8
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 300, maxHeight: '90vh'
}
}
......
......@@ -77,9 +77,9 @@ const styles = theme => ({
})
function match(content, query) {
const queries = query.split(' ')
const queries = query.toLowerCase().split(' ')
const result = queries.map(query => {
const index = content.indexOf(query)
const index = content.toLowerCase().indexOf(query)
if (index >= 0) {
return [index, index + query.length]
} else {
......
......@@ -15,6 +15,7 @@ import EditUserMetadataDialog from '../EditUserMetadataDialog'
import DownloadButton from '../DownloadButton'
import ClipboardIcon from '@material-ui/icons/Assignment'
import { CopyToClipboard } from 'react-copy-to-clipboard'
import ConfirmDialog from '../uploads/ConfirmDialog'
class DOIUnstyled extends React.Component {
static propTypes = {
......@@ -70,6 +71,10 @@ class DatasetActionsUnstyled extends React.Component {
}
})
state = {
confirmDoi: false
}
constructor(props) {
super(props)
this.handleClickDOI = this.handleClickDOI.bind(this)
......@@ -83,7 +88,7 @@ class DatasetActionsUnstyled extends React.Component {
this.props.history.push(`/dataset/id/${id}`)
}
handleClickDOI() {
handleClickDOI(after) {
const {api, dataset, onChange, raiseError} = this.props
const datasetName = dataset.name
......@@ -92,6 +97,9 @@ class DatasetActionsUnstyled extends React.Component {
if (onChange) {
onChange(dataset)
}
if (after) {
after()
}
})
.catch(raiseError)
}
......@@ -144,10 +152,23 @@ class DatasetActionsUnstyled extends React.Component {
total={dataset.total} onEditComplete={this.handleEdit}
/>}
{editable && canAssignDOI && <Tooltip title="Assign a DOI to this dataset.">
<IconButton onClick={this.handleClickDOI}>
<IconButton onClick={() => this.setState({confirmDoi: true})}>
<DOIIcon />
</IconButton>
</Tooltip>}
<ConfirmDialog
open={this.state.confirmDoi}
title="Assign a DOI"
content={`
DOIs are **permanent**. Are you sure that you want to assign a DOI to this
dataset? Once the DOI was assigned, entries cannot removed from the dataset and
the dataset cannot be deleted.
`}
onClose={() => this.setState({confirmDoi: false})}
onConfirm={() => {
this.handleClickDOI(() => this.setState({confirmDoi: false}))
}}
/>
</FormGroup>
}
}
......
......@@ -297,7 +297,7 @@ export class EntryListUnstyled extends React.Component {
const selectQuery = selected ? {calc_id: selected.join(',')} : query
const createActions = (props, moreActions) => <React.Fragment>
{example && editable ? <EditUserMetadataDialog
example={example} total={totalNumber}
example={example} total={selected === null ? totalNumber : selected.length}
onEditComplete={() => this.props.onChange()}
{...props}
/> : ''}
......@@ -312,7 +312,7 @@ export class EntryListUnstyled extends React.Component {
return (
<div className={classes.root}>
<DataTable
entityLabels={['domain.entryLabel', domain.entryLabel + 's']}
entityLabels={[domain.entryLabel, domain.entryLabelPlural]}
selectActions={selectActions}
id={row => row.calc_id}
total={total}
......
......@@ -16,14 +16,31 @@ import UploadList from './UploadsList'
import GroupList from './GroupList'
class Search extends React.Component {
static tabs = {
'entries': {
label: 'Entries',
render: (props) => <SearchEntryList {...(props || {})}/>
},
'groups': {
label: 'Grouped entries',
render: () => <SearchGroupList />
},
'uploads': {
label: 'Uploads',
render: () => <SearchUploadList />
},
'datasets': {
label: 'Datasets',
render: () => <SearchDatasetList />
}
}
static propTypes = {
classes: PropTypes.object.isRequired,
resultTab: PropTypes.string,
entryListProps: PropTypes.object,
visualization: PropTypes.string,
groups: PropTypes.bool,
datasets: PropTypes.bool,
uploads: PropTypes.bool
tabs: PropTypes.arrayOf(PropTypes.string)
}
static styles = theme => ({
......@@ -71,7 +88,7 @@ class Search extends React.Component {
static contextType = SearchContext.type
state = {
resultTab: this.resultTab || 'entries',
resultTab: 'entries',
openVisualization: this.props.visualization
}
......@@ -89,20 +106,26 @@ class Search extends React.Component {
}
}
componentDidMount() {
if ((this.props.resultTab || 'entries') !== 'entries') {
this.setState({resultTab: this.props.resultTab})
}
}
handleTabChange(tab) {
const {setRequest} = this.context
setRequest({
uploads: tab === 'uploads' ? true : undefined,
datasets: tab === 'datasets' ? true : undefined,
groups: tab === 'groups' ? true : undefined
this.setState({resultTab: tab}, () => {
setRequest({
uploads: tab === 'uploads' ? true : undefined,
datasets: tab === 'datasets' ? true : undefined,
groups: tab === 'groups' ? true : undefined
})
})
this.setState({resultTab: tab})
}
render() {
const {classes, entryListProps, groups, datasets, uploads} = this.props
const {classes, entryListProps, tabs} = this.props
const {resultTab, openVisualization} = this.state
// const {state: {request: {uploads, datasets, groups}}} = this.context
......@@ -142,28 +165,18 @@ class Search extends React.Component {
textColor="primary"
onChange={(event, value) => this.handleTabChange(value)}
>
<Tab label="Entries" value="entries" />
{groups && <Tab label="Grouped entries" value="groups" />}
{datasets && <Tab label="Datasets" value="datasets" />}
{uploads && <Tab label="Uploads" value="uploads" />}
{tabs.map(tab => <Tab
key={tab}
label={Search.tabs[tab].label}
value={tab}
/>)}
</Tabs>
<KeepState
visible={resultTab === 'entries'}
render={() => <SearchEntryList {...(entryListProps || {})}/>}
/>
{groups && <KeepState
visible={resultTab === 'groups'}
render={() => <SearchGroupList />}
/>}
{datasets && <KeepState
visible={resultTab === 'datasets'}
render={() => <SearchDatasetList />}
/>}
{uploads && <KeepState
visible={resultTab === 'uploads'}
render={() => <SearchUploadList />}
/>}
{tabs.map(tab => <KeepState
key={tab}
visible={resultTab === tab}
render={() => Search.tabs[tab].render(entryListProps)}
/>)}
</Paper>
</div>
</div>
......
......@@ -7,12 +7,13 @@ import match from 'autosuggest-highlight/match'
import parse from 'autosuggest-highlight/parse'
import Paper from '@material-ui/core/Paper'
import MenuItem from '@material-ui/core/MenuItem'
import { Chip } from '@material-ui/core'
import { Chip, IconButton, Tooltip } from '@material-ui/core'
import { nomadPrimaryColor } from '../../config'
import { withDomain } from '../domains'
import { compose } from 'recompose'
import SearchContext from '../search/SearchContext'
import { withApi } from '../api'
import ClearIcon from '@material-ui/icons/Cancel'
function renderInput(inputProps) {
......@@ -96,7 +97,13 @@ class SearchBar extends React.Component {
}
static styles = theme => ({
root: {},
root: {
display: 'flex',
alignItems: 'flex-end'
},
clearButton: {
padding: theme.spacing.unit
},
autosuggestRoot: {
position: 'relative'
},
......@@ -243,6 +250,12 @@ class SearchBar extends React.Component {
setQuery(values, true)
}