diff --git a/gui/src/components/apiV1.js b/gui/src/components/apiV1.js index 816aaed2c116543585b589dfd85dfb4babf96e27..bb9eeced40d64331915e090831bcc842c1cec06b 100644 --- a/gui/src/components/apiV1.js +++ b/gui/src/components/apiV1.js @@ -86,6 +86,7 @@ function handleApiError(e) { class Api { constructor(keycloak, setLoading) { this.keycloak = keycloak + this.user = keycloak.loadUserInfo() this.setLoading = setLoading this.axios = axios.create({ baseURL: `${apiBase}/v1` diff --git a/gui/src/components/search/NewEntryList.js b/gui/src/components/search/NewEntryList.js deleted file mode 100644 index 108aedc3470092722df22d71ccb09bb6396e186e..0000000000000000000000000000000000000000 --- a/gui/src/components/search/NewEntryList.js +++ /dev/null @@ -1,404 +0,0 @@ -/* - * 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, { useContext } from 'react' -import PropTypes from 'prop-types' -import { withStyles, Link, Typography, Tooltip, IconButton } from '@material-ui/core' -import { compose } from 'recompose' -import { withRouter } from 'react-router' -import NewDataTable from '../NewDataTable' -import Quantity from '../Quantity' -import { Link as RouterLink } from 'react-router-dom' -import NavigateNextIcon from '@material-ui/icons/NavigateNext' -import EditUserMetadataDialog from '../EditUserMetadataDialog' -import DownloadButton from '../DownloadButton' -import PublicIcon from '@material-ui/icons/Public' -import UploaderIcon from '@material-ui/icons/AccountCircle' -import SharedIcon from '@material-ui/icons/SupervisedUserCircle' -import PrivateIcon from '@material-ui/icons/VisibilityOff' -import { domainData } from '../domainData' -import { apiContext, withApi } from '../api' -import { authorList, nameList } from '../../utils' -import EntryDetails from '../entry/EntryDetails' -import { EntryButton } from '../nav/Routes' - -export function Published(props) { - const api = useContext(apiContext) - const {entry} = props - if (entry.published) { - if (entry.with_embargo) { - if (api.user && entry.uploader.user_id === api.user.sub) { - if (entry.owners.length === 1) { - return <Tooltip title="published with embargo by you and only accessible by you"> - <UploaderIcon color="error" /> - </Tooltip> - } else { - return <Tooltip title="published with embargo by you and only accessible to you and users you shared the data with"> - <SharedIcon color="error" /> - </Tooltip> - } - } else if (api.user && entry.owners.find(user => user.user_id === api.user.sub)) { - return <Tooltip title="published with embargo and shared with you"> - <SharedIcon color="error" /> - </Tooltip> - } else { - if (api.user) { - return <Tooltip title="published with embargo and not accessible by you"> - <PrivateIcon color="error" /> - </Tooltip> - } else { - return <Tooltip title="published with embargo and might become accessible after login"> - <PrivateIcon color="error" /> - </Tooltip> - } - } - } else { - return <Tooltip title="published and accessible by everyone"> - <PublicIcon color="primary" /> - </Tooltip> - } - } else { - return <Tooltip title="you have not published this entry yet"> - <UploaderIcon color="error"/> - </Tooltip> - } -} - -export class NewEntryListUnstyled extends React.Component { - static propTypes = { - classes: PropTypes.object.isRequired, - data: PropTypes.object.isRequired, - query: PropTypes.object.isRequired, - onChange: PropTypes.func, - onEdit: PropTypes.func, - history: PropTypes.any.isRequired, - order_by: PropTypes.string.isRequired, - order: PropTypes.string.isRequired, - page: PropTypes.number.isRequired, - per_page: PropTypes.number.isRequired, - editable: PropTypes.bool, - columns: PropTypes.object, - title: PropTypes.string, - actions: PropTypes.element, - showEntryActions: PropTypes.func, - selectedColumns: PropTypes.arrayOf(PropTypes.string), - user: PropTypes.object, - showAccessColumn: PropTypes.bool, - entryPagePathPrefix: PropTypes.string, - onBottom: PropTypes.func - } - - static styles = theme => ({ - root: { - height: '100%' - }, - entryDetails: { - paddingTop: theme.spacing(2), - paddingLeft: theme.spacing(2), - paddingRight: theme.spacing(2) - }, - entryDetailsContents: { - display: 'flex', - maxWidth: 1024, - margin: 'auto' - }, - entryDetailsRow: { - paddingRight: theme.spacing(2), - width: '33%' - }, - entryDetailsActions: { - display: 'flex', - flexBasis: 'auto', - flexGrow: 0, - flexShrink: 0, - justifyContent: 'flex-end', - marginBottom: theme.spacing(1), - marginTop: theme.spacing(2) - } - }) - - state = { - selected: [] - } - - static defaultColumns = { - mainfile: { - label: 'Mainfile', - render: entry => entry.mainfile, - supportsSort: true, - ellipsisFront: true, - description: 'The mainfile of this entry within its upload.' - }, - upload_time: { - label: 'Upload time', - render: entry => new Date(entry.upload_time).toLocaleString(), - supportsSort: true, - description: 'The time this entry was uploaded.' - }, - 'authors': { - label: 'Authors', - render: entry => authorList(entry), - supportsSort: true, - description: 'The authors of this entry. This includes the uploader and its co-authors.' - }, - co_authors: { - label: 'co-Authors', - render: entry => nameList(entry.authors), - supportsSort: false, - description: 'The people that this entry was co authored with' - }, - shared_with: { - label: 'Shared with', - render: entry => nameList(entry.authors), - supportsSort: false, - description: 'The people that this entry was shared with' - }, - uploader: { - label: 'Uploader', - render: entry => entry.uploader.name, - supportsSort: true, - description: 'The uploader of this entry.' - }, - comment: { - label: 'Comment', - render: entry => entry.comment, - supportsSort: false, - description: 'User provided comment on this entry' - }, - references: { - label: 'References', - render: entry => { - const refs = entry.references || [] - if (refs.length > 0) { - return ( - <div style={{display: 'inline'}}> - {refs.map((ref, i) => <span key={ref}> - <Link href={ref}>{ref}</Link>{(i + 1) < refs.length ? ', ' : <React.Fragment/>} - </span>)} - </div> - ) - } else { - return <i>no references</i> - } - }, - supportsSort: true - }, - datasets: { - label: 'Datasets', - render: entry => { - const datasets = entry.datasets || [] - if (datasets.length > 0) { - return datasets.map(dataset => dataset.name).join(', ') - } else { - return <i>no datasets</i> - } - }, - supportsSort: false, - description: 'The dataset names that this entry belongs to.' - }, - published: { - label: 'Access', - align: 'center', - render: (entry) => <Published entry={entry} /> - } - } - - // TODO was this really intentional - UNSAFE_componentWillUpdate(prevProps) { - if (prevProps.data !== this.props.data) { - this.setState({selected: []}) - } - } - - handleChange(changes) { - if (this.props.onChange) { - this.props.onChange(changes) - } - } - - handleClickCalc(calc) { - const prefix = this.props.entryPagePathPrefix || '' - const url = `${prefix}/entry/id/${calc.upload_id}/${calc.calc_id}` - this.props.history.push(url) - } - - handleChangePage = (event, page) => { - this.handleChange({page: page + 1}) - } - - handleChangeRowsPerPage = event => { - const rowsPerPage = event.target.value - this.handleChange({per_page: rowsPerPage}) - } - - renderEntryDetails(row) { - const { classes } = this.props - - return (<div className={classes.entryDetails}> - <div className={classes.entryDetailsContents}> - <div className={classes.entryDetailsRow}> - <EntryDetails data={row} /> - </div> - - <div className={classes.entryDetailsRow}> - <Quantity className={classes.entryDetailsRow} column> - <Quantity quantity='comment' placeholder='no comment' data={row} /> - <Quantity quantity='references' placeholder='no references' data={row}> - {row.references && <div style={{display: 'inline-grid'}}> - {(row.references || []).map(ref => <Typography key={ref} noWrap> - <Link href={ref}>{ref}</Link> - </Typography>)} - </div>} - </Quantity> - <Quantity quantity='authors' data={row}> - <Typography> - {authorList(row)} - </Typography> - </Quantity> - <Quantity quantity='datasets' placeholder='no datasets' data={row}> - <div> - {(row.datasets || []).map(ds => ( - <Typography key={ds.dataset_id}> - <Link component={RouterLink} to={`/dataset/id/${ds.dataset_id}`}>{ds.name}</Link> - {ds.doi ? <span> (<Link href={`https://dx.doi.org/${ds.doi}`}>{ds.doi}</Link>)</span> : <React.Fragment/>} - </Typography>))} - </div> - </Quantity> - </Quantity> - </div> - - <div className={classes.entryDetailsRow} style={{paddingRight: 0}}> - <Quantity column > - {/* <Quantity quantity="pid" label='PID' placeholder="not yet assigned" noWrap data={row} withClipboard /> */} - <Quantity quantity="entry_id" label="entry id" noWrap withClipboard data={row} /> - <Quantity quantity="raw_id" label={`raw id`} noWrap withClipboard data={row} /> - <Quantity quantity="external_id" label={`external id`} noWrap withClipboard data={row} /> - <Quantity quantity='mainfile' noWrap ellipsisFront data={row} withClipboard /> - <Quantity quantity="upload_id" label='upload id' data={row} noWrap withClipboard /> - </Quantity> - </div> - </div> - - <div className={classes.entryDetailsActions}> - {this.showEntryActions(row) && - <EntryButton color="primary" entryId={row.entry_id} uploadId={row.upload_id}> - Show raw files and archive - </EntryButton> - } - </div> - </div>) - } - - showEntryActions(row) { - const { user } = this.props - if (row.with_embargo && !(user && row.owners.find(owner => owner.user_id === user.sub))) { - return false - } else { - return !this.props.showEntryActions || this.props.showEntryActions(row) - } - } - - renderEntryActions(row, selected) { - if (this.showEntryActions(row)) { - return <Tooltip title="Show raw files and archive"> - <EntryButton - style={selected ? {color: 'white'} : null} component={IconButton} - entryId={row.entry_id} uploadId={row.upload_id} - > - <NavigateNextIcon /> - </EntryButton> - </Tooltip> - } else { - return '' - } - } - - render() { - const { classes, data, order, order_by, page, per_page, editable, title, query, actions, user, showAccessColumn, ...rest } = this.props - console.log(data) - const { selected } = this.state - const searchResultColumns = domainData.dft.searchResultColumns - const defaultSearchResultColumns = domainData.dft.defaultSearchResultColumns - - const results = data.data || [] - const total = data.pagination && data.pagination.total - const totalNumber = total || 0 - - const columns = this.props.columns || { - ...searchResultColumns, - ...NewEntryListUnstyled.defaultColumns - } - - let selectedColumns = this.props.selectedColumns - if (!selectedColumns) { - selectedColumns = [...defaultSearchResultColumns] - if (user !== undefined || showAccessColumn) { - selectedColumns.push('published') - } - selectedColumns.push('authors') - } - - const example = selected && selected.length > 0 ? results.find(d => d.calc_id === selected[0]) : results[0] - const selectQuery = (selected && selected.length > 0) ? {calc_id: selected, owner: query['owner']} : query - const createActions = (props, moreActions) => <React.Fragment> - {example && editable ? <EditUserMetadataDialog - example={example} total={selected === null ? totalNumber : selected.length} - onEditComplete={() => this.props.onEdit()} - {...props} - /> : ''} - <DownloadButton - tooltip="Download files" - {...props}/> - {moreActions} - </React.Fragment> - const selectActions = createActions({query: selectQuery, buttonProps: {color: 'secondary'}}) - const allActions = actions - - return ( - <div className={classes.root}> - <NewDataTable - entityLabels={['entry', 'entries']} - selectActions={selectActions} - id={row => row.calc_id} - total={total} - columns={columns} - selectedColumns={selectedColumns} - selectedColumnsKey="entries" - entryDetails={this.renderEntryDetails.bind(this)} - entryActions={this.renderEntryActions.bind(this)} - data={results} - order={order} - orderBy={order_by} - selected={this.state.selected} - onSelectionChanged={selection => this.setState({selected: selection})} - onOrderChanged={(order, orderBy) => this.handleChange({order: order, order_by: orderBy})} - rows={results?.length || 0} - actions={allActions} - onBottom={this.props.onBottom} - {...rest} - /> - </div> - ) - } -} - -const NewEntryList = compose( - withRouter, - withApi(false, false), - withStyles(NewEntryListUnstyled.styles))(NewEntryListUnstyled) - -export default NewEntryList diff --git a/gui/src/components/search/results/SearchResultsEntries.js b/gui/src/components/search/results/SearchResultsEntries.js index 0058106b6c7d3ef4cb0eabb2c0031a21b6061d2a..0194e3e551334a69eee37efa51d3ef3fa27af063 100644 --- a/gui/src/components/search/results/SearchResultsEntries.js +++ b/gui/src/components/search/results/SearchResultsEntries.js @@ -15,7 +15,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useState, useContext, useCallback } from 'react' +import React, { useState, useCallback } from 'react' import PropTypes from 'prop-types' import { makeStyles } from '@material-ui/core/styles' import { Link, Typography, Tooltip, IconButton, Button } from '@material-ui/core' @@ -31,7 +31,7 @@ import { domainData } from '../../domainData' import { domainComponents } from '../../domainComponents' import { authorList, nameList } from '../../../utils' import NewDataTable from '../../NewDataTable' -import { apiContext } from '../../api' +import { useApi } from '../../apiV1' import Quantity from '../../Quantity' import searchQuantities from '../../../searchQuantities' @@ -39,7 +39,7 @@ import searchQuantities from '../../../searchQuantities' * Displays the list of search results for entries. */ export function Published(props) { - const api = useContext(apiContext) + const api = useApi() const {entry} = props if (entry.published) { if (entry.with_embargo) { @@ -255,7 +255,7 @@ const SearchResultsEntries = React.memo(({ className, ...rest }) => { - // const api = useApi() + const api = useApi() // const authenticated = api.authenticated const [selected, setSelected] = useState([]) const styles = useStyles() @@ -264,14 +264,13 @@ const SearchResultsEntries = React.memo(({ const entryPagePathPrefix = undefined const showEntryActions = useCallback((row) => { - return true - // const { user } = this.props - // if (row.with_embargo && !(user && row.owners.find(owner => owner.user_id === user.sub))) { - // return false - // } else { - // return !this.props.showEntryActions || this.props.showEntryActions(row) - // } - }, []) + const user = api.user + if (row.with_embargo && !(user && row.owners.find(owner => owner.user_id === user.sub))) { + return false + } else { + return !this.props.showEntryActions || this.props.showEntryActions(row) + } + }, [api]) const handleViewEntryPage = useCallback((event, row) => { event.stopPropagation() @@ -354,7 +353,7 @@ const SearchResultsEntries = React.memo(({ const totalNumber = total || 0 const example = selected && selected.length > 0 ? data?.data.find(d => d.calc_id === selected[0]) : data?.data[0] const selectQuery = (selected && selected.length > 0) ? {calc_id: selected, owner: query['owner']} : query - const createActions = useCallback((props, moreActions) => <React.Fragment> + const createActions = useCallback((props, moreActions) => <> {example && editable ? <EditUserMetadataDialog example={example} total={selected === null ? totalNumber : selected.length} onEditComplete={() => this.props.onEdit()} @@ -364,7 +363,7 @@ const SearchResultsEntries = React.memo(({ tooltip="Download files" {...props}/> {moreActions} - </React.Fragment>, [editable, example, selected, totalNumber]) + </>, [editable, example, selected, totalNumber]) const selectActions = createActions({query: selectQuery, buttonProps: {color: 'secondary'}}) return <NewDataTable