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

Minor GUI/API fixes.

parent f7bc99ea
Pipeline #67179 passed with stages
in 19 minutes and 21 seconds
......@@ -83,7 +83,7 @@ class DataTableToolbarUnStyled extends React.Component {
const { anchorEl } = this.state
const open = Boolean(anchorEl)
const regularActions = <React.Fragment>
const regularActions = <React.Fragment>
{actions || <React.Fragment/>}
<Tooltip title="Change displayed columns">
<IconButton onClick={this.handleClick}>
......@@ -243,6 +243,10 @@ class DataTableUnStyled extends React.Component {
paddingLeft: theme.spacing.unit * 3,
paddingRight: theme.spacing.unit * 3
},
ellipsisFront: {
direction: 'rtl',
textAlign: 'left'
},
clickable: {
cursor: 'pointer'
},
......@@ -489,9 +493,12 @@ class DataTableUnStyled extends React.Component {
</TableCell> : <React.Fragment/> }
{Object.keys(columns).filter(key => selectedColumns.indexOf(key) !== -1).map((key, i) => {
const column = columns[key]
if (column.ellipsisFront) {
console.log('####################')
}
return (
<TableCell
className={clsx([classes.cell, (selectedEntry === rowId) && classes.selectedEntryCell])}
className={clsx([classes.cell, column.ellipsisFront && classes.ellipsisFront, (selectedEntry === rowId) && classes.selectedEntryCell])}
key={key}
align={column.align || 'left'}
>
......
......@@ -176,8 +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"
name: 'search', // try to prevent browsers ignore autocomplete="off"
type: 'search', // try to prevent browsers ignore autocomplete="off"
classes: {
input: classes.input
}
......@@ -397,7 +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"
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}
......@@ -582,10 +582,10 @@ class InviteUserDialogUnstyled extends React.Component {
submitEnabled: false
}
state = this.defaultState
state = {...this.defaultState}
handleClose() {
this.setState({open: false})
this.setState({...this.defaultState, open: false})
}
handleSubmit() {
......@@ -595,8 +595,16 @@ class InviteUserDialogUnstyled extends React.Component {
this.handleClose()
}).catch(error => {
// get message in quotes
const message = ('' + error).match(/'([^']+)'/)[1]
this.setState({error: message, submitting: false, submitEnabled: false})
console.error(error)
try {
let message = ('' + error).match(/'([^']+)'/)[1]
try {
message = JSON.parse(message).errorMessage
} catch (e) {}
this.setState({error: message, submitting: false, submitEnabled: false})
} catch (e) {
this.setState({error: '' + error, submitting: false, submitEnabled: false})
}
})
}
......
......@@ -17,7 +17,8 @@ class Quantity extends React.Component {
column: PropTypes.bool,
data: PropTypes.object,
quantity: PropTypes.string,
withClipboard: PropTypes.bool
withClipboard: PropTypes.bool,
ellipsisFront: PropTypes.bool
}
static styles = theme => ({
......@@ -30,6 +31,15 @@ class Quantity extends React.Component {
value: {
flexGrow: 1
},
ellipsis: {
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
},
ellipsisFront: {
direction: 'rtl',
textAlign: 'left'
},
valueAction: {},
valueActionButton: {
padding: 4
......@@ -50,24 +60,35 @@ class Quantity extends React.Component {
'& > :not(:first-child)': {
marginTop: theme.spacing.unit * 1
}
},
label: {
color: 'rgba(0, 0, 0, 0.54)',
fontSize: '0.75rem',
fontWeight: 500
}
})
render() {
const {classes, children, label, typography, loading, placeholder, noWrap, row, column, quantity, data, withClipboard} = this.props
const {classes, children, label, typography, loading, placeholder, noWrap, row, column, quantity, data, withClipboard, ellipsisFront} = this.props
let content = null
let clipboardContent = null
let valueClassName = classes.value
if (noWrap && ellipsisFront) {
valueClassName = `${valueClassName} ${classes.ellipsisFront}`
}
console.log(valueClassName)
if (!loading) {
if (!(data && quantity && !data[quantity])) {
if (!children || children.length === 0) {
const value = data && quantity ? data[quantity] : null
if (value) {
clipboardContent = value
content = <Typography noWrap={noWrap} variant={typography} className={classes.value}>
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
{value}
</Typography>
} else {
content = <Typography noWrap={noWrap} variant={typography} className={classes.value}>
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>{placeholder || 'unavailable'}</i>
</Typography>
}
......@@ -75,7 +96,7 @@ class Quantity extends React.Component {
content = children
}
} else {
content = <Typography noWrap={noWrap} variant={typography} className={classes.value}>
content = <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>{placeholder || 'unavailable'}</i>
</Typography>
}
......@@ -86,10 +107,10 @@ class Quantity extends React.Component {
} else {
return (
<div className={classes.root}>
<Typography noWrap variant="caption">{label || quantity}</Typography>
<Typography noWrap classes={{root: classes.label}} variant="caption">{label || quantity}</Typography>
<div className={classes.valueContainer}>
{loading
? <Typography noWrap={noWrap} variant={typography} className={classes.value}>
? <Typography noWrap={noWrap} variant={typography} className={valueClassName}>
<i>loading ...</i>
</Typography> : content}
{withClipboard
......
......@@ -100,7 +100,7 @@ class RepoEntryView extends React.Component {
<Quantity column>
<Quantity quantity='comment' placeholder='no comment' {...quantityProps} />
<Quantity quantity='references' placeholder='no references' {...quantityProps}>
<div style={{display:'inline-grid'}}>
<div style={{display: 'inline-grid'}}>
{(calcData.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
......@@ -138,7 +138,7 @@ class RepoEntryView extends React.Component {
{new Date(calcData.upload_time * 1000).toLocaleString()}
</Typography>
</Quantity>
<Quantity quantity='mainfile' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity='mainfile' loading={loading} noWrap ellipsisFront {...quantityProps} withClipboard />
<Quantity quantity="calc_hash" label={`${domain.entryLabel} hash`} loading={loading} noWrap {...quantityProps} />
<Quantity quantity="raw_id" label='raw id' loading={loading} noWrap {...quantityProps} withClipboard />
<Quantity quantity="external_id" label='external id' loading={loading} noWrap {...quantityProps} withClipboard />
......
......@@ -52,7 +52,8 @@ export class EntryListUnstyled extends React.Component {
label: 'Mainfile',
render: entry => entry.mainfile,
supportsSort: true,
description: 'The mainfile of this entry.'
ellipsisFront: true,
description: 'The mainfile of this entry within its upload.'
},
upload_time: {
label: 'Upload time',
......@@ -199,7 +200,7 @@ export class EntryListUnstyled extends React.Component {
<Quantity className={classes.entryDetailsRow} column>
<Quantity quantity='comment' placeholder='no comment' data={row} />
<Quantity quantity='references' placeholder='no references' data={row}>
<div style={{display:'inline-grid'}}>
<div style={{display: 'inline-grid'}}>
{(row.references || []).map(ref => <Typography key={ref} noWrap>
<a href={ref}>{ref}</a>
</Typography>)}
......@@ -227,7 +228,7 @@ export class EntryListUnstyled extends React.Component {
{/* <Quantity quantity="pid" label='PID' placeholder="not yet assigned" noWrap data={row} withClipboard /> */}
<Quantity quantity="calc_id" label={`${domain.entryLabel} id`} noWrap withClipboard data={row} />
<Quantity quantity="upload_id" label='upload id' data={row} noWrap withClipboard />
<Quantity quantity='mainfile' noWrap data={row} withClipboard />
<Quantity quantity='mainfile' noWrap ellipsisFront data={row} withClipboard />
<Quantity quantity="upload_time" label='upload time' noWrap data={row} >
<Typography noWrap>
{new Date(row.upload_time * 1000).toLocaleString()}
......@@ -302,7 +303,7 @@ export class EntryListUnstyled extends React.Component {
{...props}
/> : ''}
<DownloadButton
tooltip="Download raw files"
tooltip="Download files"
{...props}/>
{moreActions}
</React.Fragment>
......
......@@ -11,8 +11,11 @@ import { withApi } from '../api'
import { EntryListUnstyled } from './EntryList'
import MoreIcon from '@material-ui/icons/MoreHoriz'
import DownloadButton from '../DownloadButton'
import SearchContext from './SearchContext'
class GroupUnstyled extends React.Component {
static contextType = SearchContext.type
static propTypes = {
classes: PropTypes.object.isRequired,
groupHash: PropTypes.string.isRequired,
......@@ -31,7 +34,8 @@ class GroupUnstyled extends React.Component {
update() {
const {groupHash, api, raiseError} = this.props
api.search({group_hash: groupHash, per_page: 100})
const {query} = this.context.state
api.search({...query, group_hash: groupHash, per_page: 100})
.then(data => {
this.setState({entries: data.results})
})
......@@ -178,7 +182,6 @@ class GroupListUnstyled extends React.Component {
...domain.defaultSearchResultColumns,
'datasets', 'authors', 'entries']
let paginationText
if (groups_after) {
paginationText = `next ${results.length.toLocaleString()} of ${(total || 0).toLocaleString()}`
......
......@@ -53,7 +53,8 @@ class ElementUnstyled extends React.Component {
left: 2,
margin: 0,
padding: 0,
fontSize: 8
fontSize: 8,
pointerEvents: 'none'
},
count: {
position: 'absolute',
......@@ -61,7 +62,8 @@ class ElementUnstyled extends React.Component {
right: 2,
margin: 0,
padding: 0,
fontSize: 8
fontSize: 8,
pointerEvents: 'none'
}
})
......
......@@ -43,15 +43,16 @@ class PublishConfirmDialog extends React.Component {
<DialogTitle>Publish data</DialogTitle>
<DialogContent>
<Markdown>{`
If you agree the selected uploads will move out of your private staging
area into the public [NOMAD Repository](https://repository.nomad-coe.eu/NomadRepository-1.1/).
If you wish to put an embargo on your data it will last upto 36 month. Afterwards, your data will
be made public. All public data will be made available under the Creative
If you agree this upload will be published and move out of your private staging
area into the public NOMAD. This step is final. All public data will be made available under the Creative
Commons Attribution license ([CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)).
The published data will be added to the NOMAD Repository's index overnight.
Therefore, it will take until tomorrow before your data appears in the
[NOMAD Repository](https://repository.nomad-coe.eu/NomadRepository-1.1/).
If you wish, you can put an embargo on your data. Embargoed data is not
visible to others (unless explicitly shared), but you can already create
datasets and assign DOIs for data with embargo, e.g. to put it into your
unpublished paper. The embargo will last up to 36 month. Afterwards, your
data will be made publicly available. You can also lift the embargo on
entries at any time. This functionality is part of editing entries.
`}</Markdown>
<FormGroup row style={{alignItems: 'center'}}>
......@@ -157,6 +158,7 @@ class Upload extends React.Component {
},
updating: true, // it is still not complete and continuously looking for updates
showPublishDialog: false,
showDeleteDialog: false,
columns: {},
expanded: null
}
......@@ -208,8 +210,7 @@ class Upload extends React.Component {
const columns = {
mainfile: {
label: 'Mainfile',
supportsSort: true,
description: 'The path to the main output of this entry in the upload.'
supportsSort: true
},
parser: {
label: 'Parser',
......@@ -244,10 +245,10 @@ class Upload extends React.Component {
if (error) {
return <Tooltip title={tooltip}>
<Typography color="error">
{label}
</Typography>
</Tooltip>
<Typography color="error">
{label}
</Typography>
</Tooltip>
} else {
return label
}
......@@ -341,17 +342,10 @@ class Upload extends React.Component {
this.setState({showPublishDialog: false})
}
handleDeleteCancel() {
this.setState({showDeleteDialog: false})
}
onCheckboxChanged(_, checked) {
if (this.props.onCheckboxChanged) {
this.props.onCheckboxChanged(checked)
}
}
renderTitle() {
const { classes } = this.props
const { name, create_time } = this.state.upload
......
......@@ -266,7 +266,7 @@ class UsersResource(Resource):
if error is not None:
abort(400, 'Could not invite user: %s' % error)
return datamodel.User.get(email=user.email), 200
return datamodel.User.get(username=user.username), 200
def with_signature_token(func):
......
......@@ -34,6 +34,8 @@ import jwt
from flask import g, request
import basicauth
from datetime import datetime
import re
import unidecode
from nomad import config, utils
......@@ -214,6 +216,25 @@ class Keycloak():
# Do not return an error. This is the case were there are no credentials
return None
def __create_username(self, user):
if user.first_name is not None and user.last_name is not None:
user.username = '%s%s' % (user.first_name[:1], user.last_name)
elif user.last_name is not None:
user.username = user.last_name
elif '@' in user.username:
user.username = user.username.split('@')[0]
user.username = unidecode.unidecode(user.username.lower())
user.username = re.sub(r'[^0-9a-zA-Z_\-\.]+', '', user.username)
index = 1
try:
while self.get_user(username=user.username):
user.username += '%d' % index
index += 1
except KeyError:
pass
def add_user(self, user, bcrypt_password=None, invite=False):
"""
Adds the given :class:`nomad.datamodel.User` instance to the configured keycloak
......@@ -233,10 +254,13 @@ class Keycloak():
user = datamodel.User(**user)
if user.username is None or not re.match(r'^[a-zA-Z0-9_\-\.]+$', user.username):
self.__create_username(user)
keycloak_user = dict(
id=user.user_id if user.user_id != 'not set' else None,
email=user.email,
username=user.email,
username=user.username,
firstName=user.first_name,
lastName=user.last_name,
attributes=dict(
......@@ -264,12 +288,12 @@ class Keycloak():
if user.user_id != 'not_set':
try:
self._admin_client.get_user(user.user_id)
raise KeyError('User %s with given id already exists' % user.email)
return 'User %s with given id already exists' % user.email
except KeycloakGetError:
pass
if self._admin_client.get_user_id(user.email) is not None:
raise KeyError('User with email %s already exists' % user.email)
return 'User with email %s already exists' % user.email
try:
self._admin_client.create_user(keycloak_user)
......@@ -277,8 +301,11 @@ class Keycloak():
return str(e)
if invite:
# TODO send invite
pass
try:
user = self.get_user(username=user.username)
self._admin_client.send_verify_email(user_id=user.user_id)
except Exception as e:
logger.error('could not send verify email', exc_info=e)
return None
......
......@@ -50,6 +50,7 @@ python-keycloak
basicauth
inflection
docstring-parser
unidecode
# dev/ops related
setuptools
......
......@@ -164,7 +164,7 @@ class TestAuth:
for key in keys:
assert data['users'][0].get(key) is not None
def test_invite(self, api, test_user_auth):
def test_invite(self, api, test_user_auth, no_warn):
rv = api.put(
'/auth/users', headers=test_user_auth, content_type='application/json',
data=json.dumps({
......
......@@ -203,9 +203,9 @@ def test_user_uuid(handle):
test_users = {
test_user_uuid(0): dict(email='admin', user_id=test_user_uuid(0)),
test_user_uuid(1): dict(email='sheldon.cooper@nomad-coe.eu', first_name='Sheldon', last_name='Cooper', user_id=test_user_uuid(1)),
test_user_uuid(2): dict(email='leonard.hofstadter@nomad-coe.eu', first_name='Leonard', last_name='Hofstadter', user_id=test_user_uuid(2))
test_user_uuid(0): dict(username='admin', email='admin', user_id=test_user_uuid(0)),
test_user_uuid(1): dict(username='scooper', email='sheldon.cooper@nomad-coe.eu', first_name='Sheldon', last_name='Cooper', user_id=test_user_uuid(1)),
test_user_uuid(2): dict(username='lhofstadter', email='leonard.hofstadter@nomad-coe.eu', first_name='Leonard', last_name='Hofstadter', user_id=test_user_uuid(2))
}
......@@ -223,15 +223,18 @@ class KeycloakMock:
def add_user(self, user, *args, **kwargs):
self.id_counter += 1
user.user_id = test_user_uuid(self.id_counter)
self.users[user.user_id] = dict(email=user.email, first_name=user.first_name, last_name=user.last_name, user_id=user.user_id)
user.username = (user.first_name[0] + user.last_name).lower()
self.users[user.user_id] = dict(
email=user.email, username=user.username, first_name=user.first_name,
last_name=user.last_name, user_id=user.user_id)
return None
def get_user(self, user_id=None, email=None):
def get_user(self, user_id=None, username=None):
if user_id is not None:
return User(**self.users[user_id])
elif email is not None:
elif username is not None:
for user_id, user_values in self.users.items():
if user_values['email'] == email:
if user_values['username'] == username:
return User(**user_values)
raise KeyError('Only test user emails are recognized')
else:
......
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