Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
nomad-lab
nomad-FAIR
Commits
1547c456
Commit
1547c456
authored
Nov 01, 2019
by
Markus Scheidgen
Browse files
Completed first implementation of working user metadata edit button and dialog.
parent
7f5f6754
Pipeline
#62844
passed with stages
in 32 minutes and 46 seconds
Changes
51
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
.gitlab-ci.yml
View file @
1547c456
...
...
@@ -62,7 +62,7 @@ linting:
image
:
$TEST_IMAGE
script
:
-
cd /app
-
python -m pycodestyle --ignore=E501,E701 nomad tests
-
python -m pycodestyle --ignore=E501,E701
,E731
nomad tests
-
python -m pylint --load-plugins=pylint_mongoengine nomad tests
-
python -m mypy --ignore-missing-imports --follow-imports=silent --no-strict-optional nomad tests
except
:
...
...
README.md
View file @
1547c456
...
...
@@ -79,8 +79,17 @@ your browser.
Omitted versions are plain bugfix releases with only minor changes and fixes.
### v0.7.0
-
User metadata editing and datasets with DOI
-
Revised GUI lists (search results, datasets, uploads)
-
Keycloak based user management
-
no dependencies with the NOMAD CeE Repository
-
no dependencies with the NOMAD CoE Repository
### v0.6.2
-
GUI performance enhancements
-
API /raw/query endpoint takes file pattern to further filter download contents and
strips potential shared path prefixes for a cleaner download .zip
-
Stipped common path prefixes in raw file downloads
-
minor bugfixes
### v0.6.0
-
GUI URL, and API endpoint that resolves NOMAD CoE legacy PIDs
...
...
examples/external_project_parallel_upload/upload.py
View file @
1547c456
...
...
@@ -142,7 +142,7 @@ def publish_upload(upload, calc_metadata):
'comment'
:
'Data from a cool external project'
,
'references'
:
[
'http://external.project.eu'
],
# '_uploader': <nomad_user_id>, # only works if the admin user is publishing
# 'co
_
authors': [<nomad_user_id>, <nomad_user_id>, <nomad_user_id>]
# 'coauthors': [<nomad_user_id>, <nomad_user_id>, <nomad_user_id>]
# these are calc specific metadata that supercede any upload metadata
'calculations'
:
calc_metadata
}
...
...
gui/public/nomad.png
0 → 100644
View file @
1547c456
23.3 KB
gui/public/pace.css
View file @
1547c456
...
...
@@ -7,6 +7,30 @@
user-select
:
none
;
}
html
,
body
{
background
:
url('nomad.png')
no-repeat
center
center
;
min-height
:
100%
;
height
:
100%
;
width
:
100%
;
padding
:
0
;
margin
:
0
;
}
img
.bg
{
/* Set rules to fill background */
min-height
:
100%
;
min-width
:
1920px
;
/* Set up proportionate scaling */
width
:
100%
;
height
:
100%
;
/* Set up positioning */
position
:
fixed
;
top
:
0
;
left
:
0
;
}
.pace-inactive
{
display
:
none
;
}
...
...
gui/src/components/App.js
View file @
1547c456
...
...
@@ -32,6 +32,7 @@ import ResolvePID from './entry/ResolvePID'
import
DatasetPage
from
'
./DatasetPage
'
import
{
capitalize
}
from
'
../utils
'
import
{
amber
}
from
'
@material-ui/core/colors
'
import
KeepState
from
'
./KeepState
'
export
class
VersionMismatch
extends
Error
{
constructor
(
msg
)
{
...
...
@@ -140,7 +141,7 @@ class NavigationUnstyled extends React.Component {
flexGrow: 1,
backgroundColor: theme.palette.background.default,
width:
'
100
%
'
,
overflow:
'
scroll
'
overflow:
'
auto
'
},
link: {
textDecoration:
'
none
'
,
...
...
@@ -428,6 +429,11 @@ export default class App extends React.Component {
}
}
},
'
entry_query
'
: {
exact: true,
path:
'
/
entry
/
query
'
,
render: props => <EntryPage {...props} query />
},
'
dataset
'
: {
path:
'
/
dataset
/
id
/
:
datasetId
'
,
key: (props) => `dataset/id/${props.match.params.datasetId}`,
...
...
@@ -461,6 +467,7 @@ export default class App extends React.Component {
'
metainfo
'
: {
exact: true,
path:
'
/
metainfo
'
,
singleton: true,
render: props => <MetaInfoBrowser {...props} />
},
'
metainfoEntry
'
: {
...
...
@@ -471,21 +478,13 @@ export default class App extends React.Component {
}
renderChildren(routeKey, props) {
// const { match, ...rest } = props
return (
<div>
{Object.keys(this.routes)
.filter(route => this.routes[route].singleton || route === routeKey)
.map(route => (
<div
key={route.key ? route.key(props) : route}
style={{display: routeKey === route ?
'
block
'
:
'
none
'
}}
>
{this.routes[route].render(props)}
</div>
))}
</div>
<React.Fragment>
{Object.keys(this.routes).map(route => <KeepState key={route}
visible={routeKey === route}
render={(props) => this.routes[route].render(props)}
{...props} />)}
</React.Fragment>
)
}
...
...
gui/src/components/DatasetPage.js
View file @
1547c456
...
...
@@ -40,7 +40,7 @@ class DatasetPage extends React.Component {
dataset
:
{}
}
componentDidMount
()
{
update
()
{
const
{
datasetId
,
raiseError
,
api
}
=
this
.
props
api
.
search
({
owner
:
'
all
'
,
...
...
@@ -56,6 +56,16 @@ class DatasetPage extends React.Component {
})
}
componentDidMount
()
{
this
.
update
()
}
componentDidUpdate
(
prevProps
)
{
if
(
prevProps
.
api
!==
this
.
props
.
api
||
prevProps
.
datasetId
!==
this
.
props
.
datasetId
)
{
this
.
update
()
}
}
render
()
{
const
{
classes
,
datasetId
}
=
this
.
props
const
{
dataset
}
=
this
.
state
...
...
gui/src/components/EditUserMetadataDialog.js
View file @
1547c456
...
...
@@ -7,11 +7,10 @@ import DialogContent from '@material-ui/core/DialogContent'
import
DialogContentText
from
'
@material-ui/core/DialogContentText
'
import
DialogTitle
from
'
@material-ui/core/DialogTitle
'
import
PropTypes
from
'
prop-types
'
import
{
IconButton
,
Tooltip
,
withStyles
,
Paper
,
MenuItem
,
Popper
}
from
'
@material-ui/core
'
import
{
IconButton
,
Tooltip
,
withStyles
,
Paper
,
MenuItem
,
Popper
,
CircularProgress
}
from
'
@material-ui/core
'
import
EditIcon
from
'
@material-ui/icons/Edit
'
import
AddIcon
from
'
@material-ui/icons/Add
'
import
RemoveIcon
from
'
@material-ui/icons/Delete
'
import
ReactJson
from
'
react-json-view
'
import
Autosuggest
from
'
react-autosuggest
'
import
match
from
'
autosuggest-highlight/match
'
import
parse
from
'
autosuggest-highlight/parse
'
...
...
@@ -48,6 +47,15 @@ class SuggestionsTextFieldUnstyled extends React.Component {
constructor
(
props
)
{
super
(
props
)
this
.
lastRequestId
=
null
this
.
unmounted
=
false
}
componentWillUnmount
()
{
this
.
unmounted
=
true
}
componentDidMount
()
{
this
.
unmounted
=
false
}
loadSuggestions
(
value
)
{
...
...
@@ -65,12 +73,14 @@ class SuggestionsTextFieldUnstyled extends React.Component {
this
.
lastRequestId
=
setTimeout
(()
=>
{
this
.
props
.
suggestions
(
value
).
then
(
suggestions
=>
{
this
.
setState
({
isLoading
:
false
,
suggestions
:
suggestions
})
if
(
!
this
.
unmounted
)
{
this
.
setState
({
isLoading
:
false
,
suggestions
:
suggestions
})
}
})
},
10
00
)
},
2
00
)
}
state
=
{
...
...
@@ -195,7 +205,7 @@ function isURL(str) {
class
ListTextInputUnstyled
extends
React
.
Component
{
static
propTypes
=
{
classes
:
PropTypes
.
object
.
isRequired
,
values
:
PropTypes
.
arrayOf
(
PropTypes
.
string
).
isRequired
,
values
:
PropTypes
.
arrayOf
(
PropTypes
.
object
).
isRequired
,
validate
:
PropTypes
.
func
,
label
:
PropTypes
.
string
,
errorLabel
:
PropTypes
.
string
,
...
...
@@ -223,13 +233,19 @@ class ListTextInputUnstyled extends React.Component {
const
handleChange
=
(
index
,
value
)
=>
{
// TODO
if
(
onChange
)
{
onChange
([...
values
.
slice
(
0
,
index
),
value
,
...
values
.
slice
(
index
+
1
)])
const
newValues
=
[...
values
]
if
(
newValues
[
index
])
{
newValues
[
index
].
value
=
value
}
else
{
newValues
[
index
]
=
{
value
:
value
}
}
onChange
(
newValues
)
}
}
const
handleAdd
=
()
=>
{
if
(
onChange
)
{
onChange
([...
values
,
''
])
onChange
([...
values
,
{
value
:
''
}
])
}
}
...
...
@@ -240,14 +256,20 @@ class ListTextInputUnstyled extends React.Component {
}
const
Component
=
component
||
TextField
const
normalizedValues
=
values
.
length
===
0
?
[
''
]
:
values
const
normalizedValues
=
values
.
length
===
0
?
[
{
value
:
''
}
]
:
values
return
<
React
.
Fragment
>
{
normalizedValues
.
map
((
value
,
index
)
=>
{
const
error
=
validate
&&
!
validate
(
value
)
let
labelValue
=
index
===
0
?
label
:
null
{
normalizedValues
.
map
(({
value
,
message
,
success
},
index
)
=>
{
let
error
=
validate
&&
!
validate
(
value
)
let
labelValue
if
(
index
===
0
)
{
labelValue
=
label
}
if
(
error
)
{
labelValue
=
errorLabel
||
'
Bad value
'
}
else
if
(
message
)
{
labelValue
=
message
error
=
!
success
}
return
<
div
key
=
{
index
}
className
=
{
classes
.
row
}
>
<
Component
...
...
@@ -268,7 +290,7 @@ class ListTextInputUnstyled extends React.Component {
<
/IconButton> : ''
}
<
/div
>
<
div
className
=
{
classes
.
buttonContainer
}
>
{
index
+
1
===
normalizedValues
.
length
&&
normalizedValues
[
index
]
!==
''
{
index
+
1
===
normalizedValues
.
length
&&
normalizedValues
[
index
]
.
value
!==
''
?
<
IconButton
className
=
{
classes
.
button
}
size
=
"
tiny
"
onClick
=
{
handleAdd
}
>
<
AddIcon
fontSize
=
"
inherit
"
/>
<
/IconButton> : ''
}
...
...
@@ -293,46 +315,74 @@ class EditUserMetadataDialogUnstyled extends React.Component {
total
:
PropTypes
.
number
,
example
:
PropTypes
.
object
,
buttonProps
:
PropTypes
.
object
,
api
:
PropTypes
.
object
.
isRequired
api
:
PropTypes
.
object
.
isRequired
,
raiseError
:
PropTypes
.
func
.
isRequired
,
user
:
PropTypes
.
object
,
onEditComplete
:
PropTypes
.
func
,
disabled
:
PropTypes
.
bool
}
static
styles
=
theme
=>
({
dialog
:
{
width
:
'
100%
'
},
submitWrapper
:
{
margin
:
theme
.
spacing
.
unit
,
position
:
'
relative
'
},
submitProgress
:
{
position
:
'
absolute
'
,
top
:
'
50%
'
,
left
:
'
50%
'
,
marginTop
:
-
12
,
marginLeft
:
-
12
}
})
constructor
(
props
)
{
super
(
props
)
this
.
handleButtonClick
=
this
.
handleButtonClick
.
bind
(
this
)
}
state
=
{
open
:
false
,
editData
:
{
this
.
handleClose
=
this
.
handleClose
.
bind
(
this
)
this
.
handleSubmit
=
this
.
handleSubmit
.
bind
(
this
)
this
.
verifyTimer
=
null
this
.
state
=
{...
this
.
defaultState
}
this
.
editData
=
{
comment
:
''
,
references
:
[],
co
A
uthors
:
[],
shared
W
ith
:
[],
co
a
uthors
:
[],
shared
_w
ith
:
[],
datasets
:
[],
with
E
mbargo
:
true
with
_e
mbargo
:
true
}
this
.
unmounted
=
false
}
defaultState
=
{
open
:
false
,
actions
:
{},
isVerifying
:
false
,
verified
:
true
,
submitting
:
false
}
componentWillUnmount
()
{
this
.
unmounted
=
true
}
update
()
{
const
{
example
}
=
this
.
props
const
editData
=
{
this
.
editData
=
{
comment
:
example
.
comment
||
''
,
references
:
example
.
references
||
[],
co
A
uthors
:
example
.
authors
.
filter
(
author
=>
author
.
user_id
!==
example
.
uploader
.
user_id
).
map
(
author
=>
author
.
email
),
shared
W
ith
:
example
.
owners
.
filter
(
author
=>
author
.
user_id
!==
example
.
uploader
.
user_id
).
map
(
author
=>
author
.
email
),
co
a
uthors
:
example
.
authors
.
filter
(
author
=>
author
.
user_id
!==
example
.
uploader
.
user_id
).
map
(
author
=>
author
.
email
),
shared
_w
ith
:
example
.
owners
.
filter
(
author
=>
author
.
user_id
!==
example
.
uploader
.
user_id
).
map
(
author
=>
author
.
email
),
datasets
:
(
example
.
datasets
||
[]).
map
(
ds
=>
ds
.
name
),
with
E
mbargo
:
example
.
with_embargo
with
_e
mbargo
:
example
.
with_embargo
}
this
.
setState
({
editData
:
editData
})
}
componentDidMount
()
{
this
.
unmounted
=
false
this
.
update
()
}
...
...
@@ -342,6 +392,59 @@ class EditUserMetadataDialogUnstyled extends React.Component {
}
}
verify
()
{
if
(
this
.
state
.
isVerifying
)
{
return
}
if
(
this
.
verifyTimer
!==
null
)
{
clearTimeout
(
this
.
verifyTimer
)
}
this
.
setState
({
isVerifying
:
true
,
verified
:
false
})
this
.
verifyTimer
=
setTimeout
(()
=>
{
this
.
submitPromise
(
true
).
then
(
newState
=>
{
this
.
setState
(
newState
)
}).
catch
(
error
=>
{
this
.
setState
({
verified
:
false
,
isVerifying
:
false
})
return
this
.
props
.
raiseError
(
error
)
})
},
200
)
}
submitPromise
(
verify
)
{
const
{
actions
}
=
this
.
state
const
editRequest
=
{
verify
:
verify
,
actions
:
actions
}
return
this
.
props
.
api
.
edit
(
editRequest
).
then
(
data
=>
{
if
(
this
.
unmounted
)
{
return
}
const
newActions
=
{...
this
.
state
.
actions
}
let
verified
=
true
if
(
data
.
actions
)
{
Object
.
keys
(
newActions
).
forEach
(
key
=>
{
if
(
Array
.
isArray
(
newActions
[
key
]))
{
newActions
[
key
]
=
newActions
[
key
].
map
((
action
,
i
)
=>
{
verified
&=
!
data
.
actions
[
key
]
||
data
.
actions
[
key
].
success
!==
false
return
data
.
actions
[
key
]
?
{...(
data
.
actions
[
key
][
i
]
||
{}),
value
:
action
.
value
}
:
action
})
}
})
}
return
{
actions
:
newActions
,
isVerifying
:
false
,
verified
:
verified
}
})
}
handleButtonClick
()
{
const
{
open
}
=
this
.
state
if
(
!
open
)
{
...
...
@@ -351,115 +454,150 @@ class EditUserMetadataDialogUnstyled extends React.Component {
this
.
setState
({
open
:
!
open
})
}
render
()
{
const
{
classes
,
buttonProps
,
total
,
api
}
=
this
.
props
const
{
open
}
=
this
.
state
const
close
=
()
=>
this
.
setState
({
open
:
false
})
handleClose
()
{
this
.
setState
({
submitting
:
true
})
this
.
setState
({...
this
.
defaultState
})
}
handleSubmit
()
{
this
.
setState
({
submitting
:
true
})
const
handleChange
=
(
key
,
value
)
=>
{
this
.
setState
({
editData
:
{...
this
.
state
.
editData
,
[
key
]:
value
}})
this
.
submitPromise
(
false
).
then
(
newState
=>
{
if
(
this
.
props
.
onEditComplete
)
{
this
.
props
.
onEditComplete
()
}
this
.
setState
({...
newState
,
submitting
:
false
})
this
.
handleClose
()
}).
catch
(
error
=>
{
this
.
setState
({
verified
:
false
,
isVerifying
:
false
,
submitting
:
false
})
return
this
.
props
.
raiseError
(
error
)
})
}
render
()
{
const
{
classes
,
buttonProps
,
total
,
api
,
user
,
example
,
disabled
}
=
this
.
props
const
{
open
,
actions
,
verified
,
submitting
}
=
this
.
state
const
dialogEnabled
=
user
&&
example
.
uploader
.
user_id
===
user
.
sub
&&
!
disabled
const
submitEnabled
=
Object
.
keys
(
actions
).
length
&&
!
submitting
&&
verified
const
listTextInputProps
=
(
key
,
verify
)
=>
{
const
values
=
actions
[
key
]
?
actions
[
key
]
:
this
.
editData
[
key
].
map
(
value
=>
({
value
:
value
}))
return
{
id
:
key
,
fullWidth
:
true
,
values
:
values
,
onChange
:
values
=>
{
this
.
setState
({
actions
:
{...
actions
,
[
key
]:
values
}})
if
(
verify
)
{
this
.
verify
()
}
}
}
}
const
value
=
key
=>
this
.
state
.
editData
[
key
]
const
userSuggestions
=
query
=>
{
return
api
.
getUsers
(
query
)
.
then
(
result
=>
result
.
users
)
.
catch
(
(
err
)
=>
{
console
.
log
(
err
)
.
catch
(
err
=>
{
console
.
error
(
err
)
return
[]
})
}
return
(
<
React
.
Fragment
>
<
Tooltip
title
=
"
Edit user metadata
"
>
<
IconButton
{...(
buttonProps
||
{})}
onClick
=
{
this
.
handleButtonClick
}
>
<
IconButton
{...(
buttonProps
||
{})}
onClick
=
{
this
.
handleButtonClick
}
disabled
=
{
!
dialogEnabled
}
>
<
Tooltip
title
=
{
`Edit user metadata
${
dialogEnabled
?
''
:
'
. You can only edit your data.
'
}
`
}
>
<
EditIcon
/>
<
/IconButton
>
<
/Tooltip
>
<
Dialog
classes
=
{{
paper
:
classes
.
dialog
}}
open
=
{
open
}
onClose
=
{
close
}
disableBackdropClick
disableEscapeKeyDown
>
<
DialogTitle
>
Edit
the
user
metadata
of
{
total
}
entries
<
/DialogTitle
>
<
DialogContent
>
<
DialogContentText
>
TODO
better
text
<
/DialogContentText
>
<
TextField
id
=
"
comment
"
label
=
"
Comment
"
value
=
{
value
(
'
comment
'
)}
onChange
=
{
event
=>
handleChange
(
'
comment
'
,
event
.
target
.
value
)}
margin
=
"
normal
"
multiline
fullWidth
/>
<
ListTextInput
id
=
"
references
"
label
=
"
References
"
errorLabel
=
"
References must be valid URLs
"
placeholder
=
"
Add a URL reference
"
values
=
{
value
(
'
references
'
)}
onChange
=
{
values
=>
handleChange
(
'
references
'
,
values
)}
validate
=
{
isURL
}
fullWidth
/>
<
SuggestionsListTextInput
suggestions
=
{
userSuggestions
}
suggestionValue
=
{
v
=>
v
.
email
}
suggestionRendered
=
{
v
=>
`
${
v
.
name
}
(
${
v
.
email
}
)`
}
id
=
"
coAuthors
"
label
=
"
Co-authors
"
placeholder
=
"
Add a co-author by name
"
values
=
{
value
(
'
coAuthors
'
)}
onChange
=
{
values
=>
handleChange
(
'
coAuthors
'
,
values
)}
fullWidth
/>
<
SuggestionsListTextInput
suggestions
=
{
userSuggestions
}
suggestionValue
=
{
v
=>
v
.
email
}
suggestionRendered
=
{
v
=>
`
${
v
.
name
}
(
${
v
.
email
}
)`
}
id
=
"
sharedWith
"
label
=
"
Shared with
"
placeholder
=
"
Add a user by name to share with
"
values
=
{
value
(
'
sharedWith
'
)}
onChange
=
{
values
=>
handleChange
(
'
sharedWith
'
,
values
)}
fullWidth
/>
<
SuggestionsListTextInput
suggestions
=
{
prefix
=>
{
console
.
log
(
prefix
)
return
api
.
getDatasets
(
prefix
)
.
then
(
result
=>
result
.
results
.
map
(
ds
=>
ds
.
name
))
.
catch
((
err
)
=>
{
console
.
log
(
err
)
return
[]
})
}}
suggestionValue
=
{
v
=>
v
}
suggestionRendered
=
{
v
=>
v
}
id
=
"
datasets
"
label
=
"
Datasets
"
placeholder
=
"
Add a dataset
"
values
=
{
value
(
'
datasets
'
)}
onChange
=
{
values
=>
handleChange
(
'
datasets
'
,
values
)}
fullWidth
/>
<
/DialogContent
>
<
DialogContent
>
<
ReactJson
src
=
{
this
.
state
.
editData
}
enableClipboard
=
{
false
}
collapsed
=
{
0
}
/
>
<
/DialogContent
>
<
DialogActions
>
<
Button
onClick
=
{
close
}
color
=
"
primary
"
>
Cancel
<
/Button
>
<
Button
onClick
=
{
close
}
color
=
"
primary
"
>
Submit
<
/Button
>
<
/DialogActions
>
<
/Dialog
>
<
/Tooltip
>
<
/IconButton
>
{
dialogEnabled
?
<
Dialog
classes
=
{{
paper
:
classes
.
dialog
}}
open
=
{
open
}
onClose
=
{
this
.
handleClose
}
disableBackdropClick
disableEscapeKeyDown
>
<
DialogTitle
>
Edit
the
user
metadata
of
{
total
}
entries
<
/DialogTitle
>
<
DialogContent
>
<
DialogContentText
>
You
are
editing
{
total
}
{
total
===
1
?
'
entry
'
:
'
entries
'
}.
{
total
>
1
?
'
The fields are pre-filled with data from the first entry for.
'
:
''
}
Only
the
fields
that
you
change
will
be
updated
.
Be
aware
that
all
references
,
co
-
authors
,
shared_with
,
or
datasets
count
as
one
field
.
<
/DialogContentText
>
<
TextField