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