Commit 9c1d8fcf authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Updated README.md, added breadcrumb navigation.

parent 8a89eec3
Pipeline #100187 passed with stages
in 25 minutes and 30 seconds
......@@ -48,8 +48,9 @@ Omitted versions are plain bugfix releases with only minor changes and fixes.
### v0.10.3
- fixes in the VASP parser
- new tubemole parser
- new turbemole parser
- property placeholders while loading entry page
- improved UI navigation with breadcrumbs
### v0.10.2
- fixes small parser and normalizer issues
......
......@@ -77,7 +77,10 @@ function UserdataPage() {
initialRequest={{order_by: 'upload_time', uploads_grouped: true}}
initialResultTab="uploads"
availableResultTabs={['uploads', 'datasets', 'entries', ...(encyclopediaEnabled ? ['materials'] : [])]}
resultListProps={{selectedColumnsKey: 'userEntries', selectedColumns: ['formula', 'upload_time', 'mainfile', 'published', 'co_authors', 'references', 'datasets']}}
resultListProps={{
selectedColumnsKey: 'userEntries',
entryPagePathPrefix: '/userdata',
selectedColumns: ['formula', 'upload_time', 'mainfile', 'published', 'co_authors', 'references', 'datasets']}}
/>
}
......
......@@ -17,10 +17,10 @@
*/
import React, { useCallback, useContext, useEffect, useState } from 'react'
import { useLocation } from 'react-router-dom'
import { Link as RouterLink } from 'react-router-dom'
import { Typography,
AppBar as MuiAppBar, Toolbar, Link, LinearProgress, makeStyles } from '@material-ui/core'
import { allRoutes as routes } from './Routes'
import { useRoute } from './Routes'
import LoginLogout from '../LoginLogout'
import HelpDialog from '../Help'
import MainMenu from './MainMenu'
......@@ -77,14 +77,7 @@ const useAppBarStyles = makeStyles(theme => ({
export default function AppBar() {
const classes = useAppBarStyles()
const {pathname} = useLocation()
const selectedRouteKey = Object.keys(routes).find(key => {
const route = routes[key]
return pathname.startsWith(route.path)
})
const selectedRoute = routes[selectedRouteKey]
const help = selectedRoute.appBarHelp
const title = selectedRoute.appBarTitle
const {help, title, breadCrumbs} = useRoute()
return <MuiAppBar position="fixed" className={classes.root}>
<Toolbar classes={{root: classes.toolbar}} disableGutters>
......@@ -93,6 +86,11 @@ export default function AppBar() {
<img alt="The NOMAD logo" className={classes.logo} src={`${guiBase}/nomad.png`}></img>
</Link>
<Typography variant="h6" color="inherit" noWrap>
{
breadCrumbs && breadCrumbs.map((breadCrumb, index) => <React.Fragment key={index}>
<Link component={RouterLink} to={breadCrumb.path}>{breadCrumb.title}</Link>&nbsp;›&nbsp;
</React.Fragment>)
}
{title}
</Typography>
{help ? <HelpDialog color="inherit" maxWidth="md" classes={{root: classes.helpButton}} {...help}/> : ''}
......
......@@ -19,10 +19,9 @@
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel,
FormGroup,
Switch} from '@material-ui/core'
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useLocation } from 'react-router-dom'
import React, { useEffect, useMemo, useState } from 'react'
import Markdown from '../Markdown'
import { allRoutes as routes } from './Routes'
import { useRoute } from './Routes'
import { matomo } from '../App'
import { useCookies } from 'react-cookie'
import { MenuBar, MenuBarItem, MenuBarMenu } from './MenuBar'
......@@ -118,27 +117,8 @@ function Consent(moreProps) {
}
export default function MainMenu() {
// We keep the URL of those path where components keep meaningful state in the URL.
// If the menu is used to comeback, the old URL is used. Therefore, it appears as
// if the same component instance with the same state is still there.
const {pathname, search} = useLocation()
const historyRef = useRef({
search: '/search',
userdata: '/userdata'
})
const history = {...historyRef.current}
Object.keys(historyRef.current).forEach(key => {
if (pathname.startsWith('/' + key)) {
historyRef.current[key] = pathname + (search || '')
history[key] = '/' + key
}
})
const route = Object.keys(routes).find(routeKey => pathname.startsWith(routes[routeKey].path))
const routeNavPath = route && routes[route].navPath
if (routeNavPath) {
historyRef.current.navPath = routeNavPath
}
const selected = (route && routes[route].navPath) || historyRef.current.navPath || (route && routes[route].defaultNavPath) || 'publish/uploads'
const route = useRoute()
const selected = (route?.navPath) || 'publish/uploads'
return <MenuBar selected={selected}>
<MenuBarMenu name="publish" label="Publish" route="/uploads" icon={<BackupIcon/>}>
......@@ -147,13 +127,13 @@ export default function MainMenu() {
tooltip="Upload and publish new data" icon={<SearchIcon />}
/>
<MenuBarItem
label="Your data" name="userdata" route={history.userdata}
label="Your data" name="userdata" route="/userdata"
tooltip="Manage your uploaded data" icon={<UserDataIcon />}
/>
</MenuBarMenu>
<MenuBarMenu name="explore" route={history.search} icon={<SearchIcon/>}>
<MenuBarMenu name="explore" route="/search" icon={<SearchIcon/>}>
<MenuBarItem
name="search" route={history.search}
name="search" route="/search"
tooltip="Find and download data"
/>
{encyclopediaEnabled && <MenuBarItem
......
......@@ -22,7 +22,7 @@ import 'regenerator-runtime/runtime'
// import { render, screen, within } from '@testing-library/react'
// import { MemoryRouter } from 'react-router-dom'
// import MainMenu from './MainMenu'
// import { allRoutes as routes } from './Routes'
// import { routes } from './Routes'
// expect.extend({ toBeInTheDocument })
......
......@@ -17,7 +17,7 @@
*/
import React from 'react'
import { Route } from 'react-router-dom'
import { Route, useLocation } from 'react-router-dom'
import About from '../About'
import APIs from '../APIs'
import AIToolkitPage from '../aitoolkit/AIToolkitPage'
......@@ -33,63 +33,86 @@ import UploadPage, {help as uploadHelp} from '../uploads/UploadPage'
import UserdataPage, {help as userdataHelp} from '../UserdataPage'
import { ErrorBoundary } from '../errors'
export const routes = {
'faq': {
function createEntryRoute(props) {
return ({
path: '/entry',
title: 'Entry',
help: {
title: 'The entry page',
content: entryHelp
},
...props,
routes: [
{
path: '/id',
component: EntryPage
},
{
path: '/query',
exact: true,
component: EntryQuery
},
{
path: '/pid',
component: ResolvePID
}
]
})
}
const routeSpecs = [
{
path: '/faq',
exact: true,
appBarTitle: 'Frequently Asked Questions',
title: 'Frequently Asked Questions',
component: FAQ
},
'search': {
{
path: '/search',
exact: true,
appBarTitle: 'Find and Download Data',
appBarHelp: {
title: 'Search and Download Data',
help: {
title: 'How to find and download data',
content: searchHelp
},
navPath: 'explore/search',
component: SearchPage
},
'userdata': {
{
path: '/userdata',
exact: true,
appBarTitle: 'Manage Your Data',
appBarHelp: {
title: 'Manage Your Data',
help: {
title: 'How to manage your data',
content: userdataHelp
},
navPath: 'publish/userdata',
component: UserdataPage
},
'entry': {
path: '/entry',
appBarTitle: 'Entry',
appBarHelp: {
title: 'The entry page',
content: entryHelp
},
defaultNavPath: 'explore/search',
component: UserdataPage,
routes: [
createEntryRoute({
navPath: 'publish/userdata',
breadCrumbs: [
{
title: 'Your Data',
path: '/userdata'
}
]
})
]
},
createEntryRoute({
navPath: 'explore/search',
breadCrumbs: [
{
path: '/id',
component: EntryPage
},
{
path: '/query',
exact: true,
component: EntryQuery
},
{
path: '/pid',
component: ResolvePID
title: 'Search',
path: '/search'
}
]
},
'dataset': {
}),
{
path: '/dataset',
appBarTitle: 'Dataset',
defaultNavPath: 'explore/search',
title: 'Dataset',
navPath: 'explore/search',
routes: [
{
path: '/id',
......@@ -101,68 +124,93 @@ export const routes = {
}
]
},
'uploads': {
{
path: '/uploads',
exact: true,
appBarTitle: 'Upload and Publish Data',
appBarHelp: {
title: 'Upload and Publish Data',
help: {
title: 'How to upload data',
content: uploadHelp
},
navPath: 'publish/uploads',
component: UploadPage
component: UploadPage,
routes: [
createEntryRoute({
navPath: 'publish/uploads',
breadCrumbs: [
{
title: 'Uploads',
path: '/uploads'
}
]
})
]
},
'metainfo': {
{
path: '/metainfo',
appBarTitle: 'The NOMAD Meta Info',
appBarHelp: {
title: 'The NOMAD Meta Info',
help: {
title: 'About the NOMAD meta-info',
content: metainfoHelp
},
navPath: 'analyze/metainfo',
component: MetainfoPage
},
'aitoolkit': {
{
path: '/aitoolkit',
appBarTitle: 'Artificial Intelligence Toolkit',
title: 'Artificial Intelligence Toolkit',
navPath: 'analyze/aitoolkit',
component: AIToolkitPage
},
'apis': {
{
exact: true,
path: '/apis',
appBarTitle: 'APIs',
title: 'APIs',
navPath: 'analyze/apis',
component: APIs
},
'about': {
{
exact: true,
path: '/',
appBarTitle: 'About, Documentation, Getting Help',
title: 'About, Documentation, Getting Help',
navPath: 'about/info',
component: About
}
}
]
export const allRoutes = Object.keys(routes).map(key => {
const route = routes[key]
return route.routes
? route.routes.map(subRoute => ({
function flattenRouteSpecs(routeSpecs, parent, results) {
results = results || []
parent = parent || {}
routeSpecs.forEach(route => {
const flatRoute = {
...parent,
component: null,
exact: false,
...route,
...subRoute,
path: `${route.path}/${subRoute.path.replace(/^\/+/, '')}`,
path: parent.path ? `${parent.path}/${route.path.replace(/^\/+/, '')}` : route.path,
routes: undefined
}))
: route
}).flat()
}
if (flatRoute.component) {
results.push(flatRoute)
}
if (route.routes) {
flattenRouteSpecs(route.routes, flatRoute, results)
}
})
return results
}
export const routes = flattenRouteSpecs(routeSpecs)
routes.sort((a, b) => (a.path > b.path) ? -1 : 1)
export default function Routes() {
return <React.Fragment>
{Object.keys(allRoutes).map(routeKey => {
const route = allRoutes[routeKey]
const { path, exact } = route
{routes.map(route => {
const {path, exact} = route
const children = childProps => childProps.match && <route.component {...childProps} />
return <ErrorBoundary key={routeKey}>
return <ErrorBoundary key={path}>
<Route exact={exact} path={path}
// eslint-disable-next-line react/no-children-prop
children={children}
......@@ -171,3 +219,16 @@ export default function Routes() {
})}
</React.Fragment>
}
export function useRoute() {
const {pathname, search} = useLocation()
routes.forEach(route => {
(route.breadCrumbs || []).forEach(breadCrumb => {
if (breadCrumb.path.startsWith(pathname)) {
breadCrumb.path = pathname + (search || '')
}
})
})
const route = routes.find(route => pathname.startsWith(route.path))
return route
}
......@@ -96,7 +96,8 @@ export class EntryListUnstyled extends React.Component {
selectedColumns: PropTypes.arrayOf(PropTypes.string),
domain: PropTypes.object,
user: PropTypes.object,
showAccessColumn: PropTypes.bool
showAccessColumn: PropTypes.bool,
entryPagePathPrefix: PropTypes.string
}
static styles = theme => ({
......@@ -227,7 +228,9 @@ export class EntryListUnstyled extends React.Component {
}
handleClickCalc(calc) {
this.props.history.push(`/entry/id/${calc.upload_id}/${calc.calc_id}`)
const prefix = this.props.entryPagePathPrefix || ''
const url = `${prefix}/entry/id/${calc.upload_id}/${calc.calc_id}`
this.props.history.push(url)
}
handleChangePage = (event, page) => {
......@@ -317,7 +320,9 @@ export class EntryListUnstyled extends React.Component {
handleViewEntryPage(event, row) {
event.stopPropagation()
this.props.history.push(`/entry/id/${row.upload_id}/${row.calc_id}`)
const prefix = this.props.entryPagePathPrefix || ''
const url = `${prefix}/entry/id/${row.upload_id}/${row.calc_id}`
this.props.history.push(url)
}
renderEntryActions(row, selected) {
......
......@@ -246,8 +246,7 @@ class Upload extends React.Component {
showPublishDialog: false,
showPublishToCentralNomadDialog: false,
showDeleteDialog: false,
columns: {},
expanded: null
columns: {}
}
_unmounted = false
......@@ -267,10 +266,6 @@ class Upload extends React.Component {
}
componentDidUpdate(prevProps, prevState) {
if (prevProps.open !== this.props.open && this.props.open) {
this.setState({expanded: null})
}
if (prevProps.domain !== this.props.domain) {
this.updateColumns()
}
......@@ -710,6 +705,7 @@ class Upload extends React.Component {
onEdit={this.handleChange}
actions={actions}
showEntryActions={entry => entry.processed || !running}
entryPagePathPrefix="/uploads"
{...this.state.params}
/>
}
......@@ -737,17 +733,18 @@ class Upload extends React.Component {
render() {
const { classes, open } = this.props
const { upload, showPublishDialog, showDeleteDialog, showPublishToCentralNomadDialog, expanded } = this.state
const { upload, showPublishDialog, showDeleteDialog, showPublishToCentralNomadDialog } = this.state
const { errors, last_status_message } = upload
if (this.state.upload) {
return (
<div className={classes.root}>
<Accordion
expanded={expanded === null ? open : expanded}
onChange={(event, expanded) => {
this.setState({expanded: expanded})
expanded={open}
onChange={(event, open) => {
if (open) {
this.props.history.push(`/uploads?open=${upload.upload_id}`)
} else {
this.props.history.push('/uploads')
}
}}
......
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