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

Removed v0 api from GUI. #607

parent 4a4eae50
Pipeline #110176 passed with stages
in 29 minutes and 23 seconds
......@@ -15,18 +15,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useContext, useLayoutEffect, useRef, useCallback, useEffect, useState } from 'react'
import React, { useLayoutEffect, useRef, useCallback, useEffect, useState } from 'react'
import {ReactComponent as AboutSvg} from './about.svg'
import PropTypes from 'prop-types'
import Markdown from './Markdown'
import { appBase, debug, consent, aitoolkitEnabled, encyclopediaEnabled } from '../config'
import { apiContext } from './api'
import packageJson from '../../package.json'
import { domainData } from './domainData'
import { Grid, Card, CardContent, Typography, makeStyles, Link, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core'
import { Link as RouterLink, useHistory } from 'react-router-dom'
import tutorials from '../toolkitMetadata'
import parserMetadata from '../parserMetadata'
import { useApi } from './api'
function CodeInfo({code, ...props}) {
if (!code) {
......@@ -166,7 +166,7 @@ const useStyles = makeStyles(theme => ({
export default function About() {
const classes = useStyles()
const {info} = useContext(apiContext)
const {info} = useApi()
const svg = useRef()
const history = useHistory()
......
......@@ -28,7 +28,6 @@ import { nomadTheme, matomoEnabled, matomoUrl, matomoSiteId, keycloakBase, keycl
import Keycloak from 'keycloak-js'
import { KeycloakProvider } from 'react-keycloak'
import { MuiThemeProvider } from '@material-ui/core/styles'
import { ApiProvider } from './api'
import { ErrorSnacks, ErrorBoundary } from './errors'
import Navigation from './nav/Navigation'
......@@ -57,9 +56,7 @@ export default function App() {
<ErrorSnacks>
<ErrorBoundary>
<RecoilRoot>
<ApiProvider>
<Navigation />
</ApiProvider>
<Navigation />
</RecoilRoot>
</ErrorBoundary>
</ErrorSnacks>
......
......@@ -19,7 +19,7 @@ import React, { useContext, useState, useEffect, useMemo } from 'react'
import PropTypes from 'prop-types'
import { Typography, makeStyles } from '@material-ui/core'
import { errorContext } from './errors'
import { useApi } from './apiV1'
import { useApi } from './api'
import Search from './search/Search'
import { SearchContext } from './search/SearchContext'
import { DOI } from './search/results/DatasetList'
......
......@@ -15,111 +15,103 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import React, { useState } from 'react'
import PropTypes from 'prop-types'
import FileSaver from 'file-saver'
import { withApi } from './api'
import { compose } from 'recompose'
import { withErrors } from './errors'
import { useErrors } from './errors'
import { apiBase } from '../config'
import { Tooltip, IconButton, Menu, MenuItem } from '@material-ui/core'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import { useApi } from './api'
class DownloadButton extends React.Component {
static 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,
api: PropTypes.object.isRequired,
user: PropTypes.object,
raiseError: PropTypes.func.isRequired,
dark: PropTypes.bool
}
const DownloadButton = React.memo(function DownloadButton(props) {
const {tooltip, disabled, buttonProps, dark, query} = props
const {api, user} = useApi()
const {raiseError} = useErrors()
state = {
preparingDownload: false,
anchorEl: null
}
const [preparingDownload, setPreparingDownload] = useState(false)
const [anchorEl, setAnchorEl] = useState(null)
handleClick(event) {
const handleClick = event => {
event.stopPropagation()
this.setState({ anchorEl: event.currentTarget })
setAnchorEl(event.currentTarget)
}
async handleSelect(choice) {
const {api, query, user, raiseError} = this.props
const params = {}
Object.keys(query).forEach(key => { params[key] = query[key] })
if (user) {
try {
const token = await api.getSignatureToken()
params['signature_token'] = token
} catch (e) {
this.setState({preparingDownload: false})
raiseError(e)
}
}
const handleSelect = (choice) => {
setAnchorEl(null)
// TODO using split/append is not consistent for all parameters, using append here
const urlSearchParams = new URLSearchParams()
Object.keys(params).forEach(key => {
const value = params[key]
Object.keys(query).forEach(key => {
const value = query[key]
if (Array.isArray(value)) {
value.forEach(item => urlSearchParams.append(key, item))
} else {
urlSearchParams.append(key, value)
}
})
const url = `${apiBase}/${choice}/${choice === 'archive' ? 'download' : 'query'}?${urlSearchParams.toString()}`
FileSaver.saveAs(url, `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, anchorEl} = this.state
const openDownload = () => {
const url = `${apiBase}/${choice}/${choice === 'archive' ? 'download' : 'query'}?${urlSearchParams.toString()}`
FileSaver.saveAs(url, `nomad-${choice}-download.zip`)
}
const props = {
...buttonProps,
disabled: disabled || preparingDownload,
onClick: this.handleClick.bind(this)
if (user) {
setPreparingDownload(true)
api.get('/auth/signature_token')
.then(response => {
urlSearchParams.append('signature_token', response.signature_token)
openDownload()
})
.catch(raiseError)
.finally(setPreparingDownload(false))
} else {
openDownload()
}
}
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>
const handleClose = () => {
setAnchorEl(null)
}
return <React.Fragment>
<IconButton
{...buttonProps}
disabled={disabled || preparingDownload}
onClick={handleClick}
style={dark ? {color: 'white'} : null}
>
<Tooltip title={tooltip || 'Download'}>
<DownloadIcon />
</Tooltip>
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={() => handleSelect('raw')}>Raw uploaded files</MenuItem>
<MenuItem onClick={() => handleSelect('archive')}>NOMAD Archive files</MenuItem>
</Menu>
</React.Fragment>
})
DownloadButton.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 compose(withApi(false), withErrors)(DownloadButton)
export default DownloadButton
......@@ -25,14 +25,13 @@ 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, CircularProgress,
FormGroup, Checkbox, FormLabel } from '@material-ui/core'
FormGroup, Checkbox } 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 Autosuggest from 'react-autosuggest'
import match from 'autosuggest-highlight/match'
import parse from 'autosuggest-highlight/parse'
import { compose } from 'recompose'
import { withApi } from './api'
const local_users = {}
......@@ -269,9 +268,9 @@ class UserInputUnstyled extends React.Component {
const {api} = this.props
query = query.toLowerCase()
return api.getUsers(query)
.then(result => {
result.users.forEach(user => update_local_user(user))
const withQueryInName = result.users.filter(
.then(users => {
users.forEach(user => update_local_user(user))
const withQueryInName = users.filter(
user => user.name.toLowerCase().indexOf(query) !== -1)
withQueryInName.sort((a, b) => {
const aValue = a.name.toLowerCase()
......@@ -322,7 +321,7 @@ class UserInputUnstyled extends React.Component {
}
}
const UserInput = withApi(false)(UserInputUnstyled)
const UserInput = withApi(UserInputUnstyled)
class DatasetInputUnstyled extends React.Component {
static propTypes = {
......@@ -373,7 +372,7 @@ class DatasetInputUnstyled extends React.Component {
}
}
const DatasetInput = withApi(false)(DatasetInputUnstyled)
const DatasetInput = withApi(DatasetInputUnstyled)
class ReferenceInput extends React.Component {
static propTypes = {
......@@ -479,7 +478,7 @@ class ListTextInputUnstyled extends React.Component {
values: PropTypes.arrayOf(PropTypes.object).isRequired,
label: PropTypes.string,
onChange: PropTypes.func,
component: PropTypes.func
component: PropTypes.any
}
static styles = theme => ({
......@@ -611,15 +610,10 @@ class InviteUserDialogUnstyled extends React.Component {
this.props.api.inviteUser(this.state.data).then(() => {
this.handleClose()
}).catch(error => {
// get message in quotes
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) {
const detail = error?.response?.data?.detail
if (detail) {
this.setState({error: detail, submitting: false, submitEnabled: false})
} else {
this.setState({error: '' + error, submitting: false, submitEnabled: false})
}
})
......@@ -692,7 +686,7 @@ class InviteUserDialogUnstyled extends React.Component {
}
}
const InviteUserDialog = compose(withApi(true, false), withStyles(InviteUserDialogUnstyled.styles))(InviteUserDialogUnstyled)
const InviteUserDialog = withApi(withStyles(InviteUserDialogUnstyled.styles)(InviteUserDialogUnstyled))
class UserMetadataFieldUnstyled extends React.PureComponent {
static propTypes = {
......@@ -749,7 +743,6 @@ class EditUserMetadataDialogUnstyled extends React.Component {
disabled: PropTypes.bool,
title: PropTypes.string,
info: PropTypes.object,
withoutLiftEmbargo: PropTypes.bool,
text: PropTypes.string
}
......@@ -770,9 +763,6 @@ class EditUserMetadataDialogUnstyled extends React.Component {
left: '50%',
marginTop: -12,
marginLeft: -12
},
liftEmbargoLabel: {
marginTop: theme.spacing(3)
}
})
......@@ -788,10 +778,7 @@ class EditUserMetadataDialogUnstyled extends React.Component {
references: [],
coauthors: [],
shared_with: [],
datasets: [],
with_embargo: {
value: 'lift'
}
datasets: []
}
this.unmounted = false
}
......@@ -1019,7 +1006,7 @@ class EditUserMetadataDialogUnstyled extends React.Component {
return (
<React.Fragment>
{!this.props.text &&
<IconButton {...buttonProps}>
<IconButton {...allButtonProps}>
<Tooltip {...tooltipProps}>
<EditIcon />
</Tooltip>
......@@ -1085,9 +1072,6 @@ class EditUserMetadataDialogUnstyled extends React.Component {
label="Datasets"
/>
</UserMetadataField>
{!this.props.withoutLiftEmbargo && <UserMetadataField classes={{container: classes.liftEmbargoLabel}} {...metadataFieldProps('with_embargo', true, 'lift')}>
<FormLabel>Lift embargo</FormLabel>
</UserMetadataField>}
</DialogContent>
{this.renderDialogActions(submitting, submitEnabled)}
</Dialog>
......@@ -1126,4 +1110,4 @@ class EditUserMetadataDialogUnstyled extends React.Component {
}
}
export default compose(withApi(false, false), withStyles(EditUserMetadataDialogUnstyled.styles))(EditUserMetadataDialogUnstyled)
export default withApi(withStyles(EditUserMetadataDialogUnstyled.styles)(EditUserMetadataDialogUnstyled))
......@@ -17,70 +17,66 @@
*/
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core/styles'
import Typography from '@material-ui/core/Typography'
import { compose } from 'recompose'
import { Button, Link } from '@material-ui/core'
import { withApi } from './api'
import { Button, Link, makeStyles } from '@material-ui/core'
import { keycloakBase, keycloakRealm } from '../config'
import LoginIcon from '@material-ui/icons/AccountCircle'
import { useApi } from './api'
import { useKeycloak } from 'react-keycloak'
class LoginLogout extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
variant: PropTypes.string,
color: PropTypes.string,
user: PropTypes.object,
keycloak: PropTypes.object.isRequired
}
static styles = theme => ({
root: {
display: 'flex',
alignItems: 'center',
'& p': {
marginRight: theme.spacing(2)
}
},
link: {
textDecoration: 'underline'
},
button: {} // to allow overrides
})
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
alignItems: 'center',
'& p': {
marginRight: theme.spacing(2)
}
},
link: {
textDecoration: 'underline'
},
button: {} // to allow overrides
}))
render() {
const { classes, variant, color, keycloak, user } = this.props
const LoginLogout = React.memo(function LoginLogout(props) {
const classes = useStyles()
const {user} = useApi()
const {keycloak} = useKeycloak()
const {variant, color} = props
if (keycloak.authenticated) {
return (
<div className={classes.root}>
<Typography color="primary" variant="body1">
Welcome <Link
className={classes.link}
href={`${keycloakBase.replace(/\/$/, '')}/realms/${keycloakRealm}/account/`}>
{ user ? user.name : '...' }
</Link>
</Typography>
<Button
className={classes.button} style={{marginLeft: 8}}
variant={variant} color={color}
onClick={() => keycloak.logout()}
startIcon={<LoginIcon/>}
>Logout</Button>
</div>
)
} else {
return (
<div className={classes.root}>
<Button
className={classes.button} variant={variant} color={color}
startIcon={<LoginIcon/>}
onClick={() => keycloak.login()}
>Login / Register</Button>
</div>
)
}
if (keycloak.authenticated) {
return (
<div className={classes.root}>
<Typography color="primary" variant="body1">
Welcome <Link
className={classes.link}
href={`${keycloakBase.replace(/\/$/, '')}/realms/${keycloakRealm}/account/`}>
{ user ? user.name : '...' }
</Link>
</Typography>
<Button
className={classes.button} style={{marginLeft: 8}}
variant={variant} color={color}
onClick={() => keycloak.logout()}
startIcon={<LoginIcon/>}
>Logout</Button>
</div>
)
} else {
return (
<div className={classes.root}>
<Button
className={classes.button} variant={variant} color={color}
startIcon={<LoginIcon/>}
onClick={() => keycloak.login()}
>Login / Register</Button>
</div>
)
}
})
LoginLogout.propTypes = {
variant: PropTypes.string,
color: PropTypes.string
}
export default compose(withApi(false), withStyles(LoginLogout.styles))(LoginLogout)
export default LoginLogout
......@@ -16,7 +16,7 @@
* limitations under the License.
*/
import React from 'react'
import { withLoginRequired } from './apiV1'
import { withLoginRequired } from './api'
import { SearchContext } from './search/SearchContext'
import Search from './search/Search'
......
This diff is collapsed.
/*
* 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, { useMemo } from 'react'
import {
atom,
useSetRecoilState,
useRecoilValue,
useRecoilState
} from 'recoil'
import PropTypes from 'prop-types'
import { apiBase } from '../config'
import { makeStyles, Typography } from '@material-ui/core'
import LoginLogout from './LoginLogout'
import { useKeycloak } from 'react-keycloak'
import axios from 'axios'
export class DoesNotExist extends Error {
constructor(msg) {
super(msg)
this.name = 'DoesNotExist'
}
}
export class NotAuthorized extends Error {
constructor(msg) {
super(msg)
this.name = 'NotAuthorized'
}
}
export class ApiError extends Error {
constructor(msg) {
super(msg)
this.name = 'CannotReachApi'
}
}
function handleApiError(e) {
if (e.name === 'CannotReachApi' || e.name === 'NotAuthorized' || e.name === 'DoesNotExist') {
throw e
}
let error = null
if (e.response) {
const body = e.response.body
const message = (body && (body.message || body.description)) || e.response.statusText
const errorMessage = `${message} (${e.response.status})`
if (e.response.status === 404) {
error = new DoesNotExist(errorMessage)
} else if (e.response.status === 401) {
error = new NotAuthorized(errorMessage)
} else if (e.response.status === 502) {
error = new ApiError(errorMessage)
} else {
error = new Error(errorMessage)
}
error.status = e.response.status