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

Added help system.

parent a9315274
......@@ -11,6 +11,7 @@
"html-to-react": "^1.3.3",
"marked": "^0.6.0",
"react": "^16.4.2",
"react-cookie": "^3.0.8",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.4.2",
"react-dropzone": "^5.0.1",
......
......@@ -8,30 +8,68 @@ import Repo from './Repo'
import Documentation from './Documentation'
import Development from './Development'
import Home from './Home'
import { HelpContext } from './Help'
import { withCookies, Cookies } from 'react-cookie'
import { instanceOf } from 'prop-types'
function App() {
return (
<MuiThemeProvider theme={genTheme}>
<BrowserRouter basename={appBase}>
<Navigation>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/repo" component={Repo} />
{/* <Route path="/repo/:uploadId/:calcId" component={RepoCalc} /> */}
<Route path="/upload" component={Uploads} />
<Route exact path="/archive" render={() => <div>Archive</div>} />
{/* <Route path="/archive/:uploadId/:calcId" component={ArchiveCalc} /> */}
<Route path="/enc" render={() => <div>{'In the future, you\'ll see charts\'n\'stuff for your calculations and materials.'}</div>} />
<Route path="/analytics" render={() => <div>{'In the future, you\'ll see analytics notebooks here.'}</div>} />
<Route path="/profile" render={() => <div>Profile</div>} />
<Route path="/docs" component={Documentation} />
<Route path="/dev" component={Development} />
<Route render={() => <div>Not found</div>} />
</Switch>
</Navigation>
</BrowserRouter>
</MuiThemeProvider>
)
class App extends React.Component {
static propTypes = {
cookies: instanceOf(Cookies).isRequired
}
state = {
helpCookies: [],
allHelpCookies: [],
allClosed: () => this.state.helpCookies.length === this.state.allHelpCookies.length,
someClosed: () => this.state.helpCookies.length !== 0,
isOpen: (cookie) => {
if (this.state.allHelpCookies.indexOf(cookie) === -1) {
this.state.allHelpCookies.push(cookie)
}
return this.state.helpCookies.indexOf(cookie) === -1
},
gotIt: (cookie) => {
const updatedHelpCookies = [...this.state.helpCookies, cookie]
this.props.cookies.set('help', updatedHelpCookies)
this.setState({helpCookies: updatedHelpCookies})
},
switchHelp: () => {
const updatedCookies = this.state.someClosed() ? [] : this.state.allHelpCookies
this.setState({helpCookies: updatedCookies})
this.props.cookies.set('help', updatedCookies)
}
}
componentDidMount() {
this.setState({helpCookies: this.props.cookies.get('help') || []})
}
render() {
return (
<MuiThemeProvider theme={genTheme}>
<BrowserRouter basename={appBase}>
<HelpContext.Provider value={this.state}>
<Navigation>
<Switch>
<Route exact path="/" component={Home} />
<Route exact path="/repo" component={Repo} />
{/* <Route path="/repo/:uploadId/:calcId" component={RepoCalc} /> */}
<Route path="/upload" component={Uploads} />
<Route exact path="/archive" render={() => <div>Archive</div>} />
{/* <Route path="/archive/:uploadId/:calcId" component={ArchiveCalc} /> */}
<Route path="/enc" render={() => <div>{'In the future, you\'ll see charts\'n\'stuff for your calculations and materials.'}</div>} />
<Route path="/analytics" render={() => <div>{'In the future, you\'ll see analytics notebooks here.'}</div>} />
<Route path="/profile" render={() => <div>Profile</div>} />
<Route path="/docs" component={Documentation} />
<Route path="/dev" component={Development} />
<Route render={() => <div>Not found</div>} />
</Switch>
</Navigation>
</HelpContext.Provider>
</BrowserRouter>
</MuiThemeProvider>
)
}
}
export default App
export default withCookies(App)
import React from 'react'
import { withStyles, Button } from '@material-ui/core'
import Markdown from './Markdown'
import PropTypes from 'prop-types'
export class HelpManager {
isOpen(helpKey) {
return true
}
gotIt(helpKey) {
}
}
export const HelpContext = React.createContext(new HelpManager())
class HelpComponent extends React.Component {
static styles = theme => ({
root: {
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2,
borderRadius: theme.spacing.unit * 0.5,
border: `1px solid ${theme.palette.primary.main}`,
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
},
content: {
paddingLeft: theme.spacing.unit * 2,
flex: '1 1 auto'
},
actions: {
padding: theme.spacing.unit,
flex: '0 0 auto'
}
})
static propTypes = {
classes: PropTypes.object.isRequired,
children: PropTypes.any,
cookie: PropTypes.string.isRequired
}
render() {
const { classes, children, cookie } = this.props
return (
<HelpContext.Consumer>{
help => (
help.isOpen(cookie)
? <div className={classes.root}>
<div className={classes.content}>
<Markdown>
{children}
</Markdown>
</div>
<div className={classes.actions}>
<Button color="primary" onClick={() => help.gotIt(cookie)}>Got it</Button>
</div>
</div> : ''
)
}</HelpContext.Consumer>
)
}
}
export const Help = withStyles(HelpComponent.styles)(HelpComponent)
......@@ -3,13 +3,12 @@ import PropTypes from 'prop-types'
import { withStyles } from '@material-ui/core'
import Markdown from './Markdown'
class Home extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired
}
static styles = theme => ({
root: {},
root: {}
});
render() {
......
......@@ -23,7 +23,7 @@ var styles = theme => ({
'& pre, & pre[class*="language-"]': {
margin: '24px 0',
padding: '12px 18px',
backgroundColor: theme.palette.background.paper,
backgroundColor: theme.palette.primary[50],
borderRadius: theme.shape.borderRadius,
overflow: 'auto',
WebkitOverflowScrolling: 'touch' // iOS momentum scrolling.
......@@ -35,30 +35,30 @@ var styles = theme => ({
fontFamily: 'Consolas, "Liberation Mono", Menlo, Courier, monospace',
padding: '3px 6px',
color: theme.palette.text.primary,
backgroundColor: theme.palette.background.paper,
backgroundColor: theme.palette.primary[50],
fontSize: 14
},
'& p code, & ul code, & pre code': {
fontSize: 14,
lineHeight: 1.6
},
'& h1': (0, extend)({}, theme.typography.display2, {
color: theme.palette.text.secondary,
'& h1': (0, extend)({}, theme.typography.h3, {
color: theme.palette.text.primary,
margin: '32px 0 16px'
}),
'& .description': (0, extend)({}, theme.typography.headline, {
'& .description': (0, extend)({}, theme.typography.h5, {
margin: '0 0 40px'
}),
'& h2': (0, extend)({}, theme.typography.display1, {
color: theme.palette.text.secondary,
'& h2': (0, extend)({}, theme.typography.h4, {
color: theme.palette.text.primary,
margin: '32px 0 24px'
}),
'& h3': (0, extend)({}, theme.typography.headline, {
color: theme.palette.text.secondary,
'& h3': (0, extend)({}, theme.typography.h5, {
color: theme.palette.text.primary,
margin: '32px 0 24px'
}),
'& h4': (0, extend)({}, theme.typography.title, {
color: theme.palette.text.secondary,
'& h4': (0, extend)({}, theme.typography.h6, {
color: theme.palette.text.primary,
margin: '24px 0 16px'
}),
'& p, & ul, & ol': {
......
......@@ -23,10 +23,11 @@ import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'
import MenuIcon from '@material-ui/icons/Menu'
import { Link, withRouter } from 'react-router-dom'
import { compose } from 'recompose'
import { Avatar, MuiThemeProvider, IconButton } from '@material-ui/core'
import { genTheme, repoTheme, archiveTheme, encTheme, appStaticBase, analyticsTheme } from '../config'
import { MuiThemeProvider, IconButton, Button, Checkbox, FormLabel } from '@material-ui/core'
import { genTheme, repoTheme, archiveTheme, encTheme, analyticsTheme } from '../config'
import { ErrorSnacks } from './errors'
import classNames from 'classnames'
import { HelpContext } from './Help'
const drawerWidth = 200
......@@ -153,6 +154,20 @@ class Navigation extends React.Component {
},
menuItem: {
paddingLeft: theme.spacing.unit * 3
},
barActions: {
display: 'flex',
alignItems: 'center',
'& p': {
marginRight: theme.spacing.unit * 2
},
'& button': {
borderColor: theme.palette.getContrastText(theme.palette.primary.main),
marginRight: theme.spacing.unit * 4
}
},
barSelect: {
color: `${theme.palette.getContrastText(theme.palette.primary.main)} !important`
}
})
......@@ -294,10 +309,22 @@ class Navigation extends React.Component {
>
<MenuIcon />
</IconButton>
<Typography variant="title" color="inherit" noWrap className={classes.flex}>
<Typography variant="h6" color="inherit" noWrap className={classes.flex}>
{selected(toolbarTitles)}
</Typography>
<Avatar src={`${appStaticBase}/me.jpg`}/>
<div className={classes.barActions}>
<Typography color="inherit" variant="body1">Welcome, Markus</Typography>
<Button color="inherit" variant="outlined">Logout</Button>
<FormLabel className={classes.barSelect} >Show help</FormLabel>
<HelpContext.Consumer>{
help => (
<Checkbox
checked={!help.someClosed()} indeterminate={!help.allClosed() && help.someClosed()}
onClick={() => help.switchHelp()}
classes={{root: classes.barSelect, checked: classes.barSelect}} />
)
}</HelpContext.Consumer>
</div>
</Toolbar>
</AppBar>
</MuiThemeProvider>
......
......@@ -11,8 +11,10 @@ import Upload from './Upload'
import { withErrors } from './errors'
import { compose } from 'recompose'
import DeleteIcon from '@material-ui/icons/Delete'
import ReloadIcon from '@material-ui/icons/Cached'
import CheckIcon from '@material-ui/icons/Check'
import ConfirmDialog from './ConfirmDialog'
import { Help } from './Help'
class Uploads extends React.Component {
static propTypes = {
......@@ -25,7 +27,9 @@ class Uploads extends React.Component {
width: '100%'
},
dropzoneContainer: {
height: 192
height: 192,
marginTop: theme.spacing.unit * 2,
marginBottom: theme.spacing.unit * 2
},
dropzone: {
textAlign: 'center',
......@@ -65,7 +69,7 @@ class Uploads extends React.Component {
state = {
uploads: null,
uploadCommand: null,
uploadCommand: 'loading ...',
selectedUploads: [],
loading: true,
showAccept: false
......@@ -159,52 +163,61 @@ class Uploads extends React.Component {
renderUploads() {
const { classes } = this.props
const { uploads, selectedUploads } = this.state
if (uploads && uploads.length > 0) {
return (
<div>
<div style={{width: '100%'}}>
<Markdown text={'These are the *existing* uploads:'} />
<FormGroup className={classes.selectFormGroup} row>
<FormControlLabel label="all" style={{flexGrow: 1}} control={(
<Checkbox
checked={selectedUploads.length === uploads.length}
onChange={(_, checked) => this.onSelectionAllChanged(checked)}
/>
)} />
<FormLabel classes={{root: classes.selectLabel}}>
{`selected uploads ${selectedUploads.length}/${uploads.length}`}
</FormLabel>
<IconButton
disabled={selectedUploads.length === 0}
onClick={this.onDeleteClicked.bind(this)}
>
<DeleteIcon />
</IconButton>
<IconButton disabled={selectedUploads.length === 0} onClick={this.onAcceptClicked.bind(this)}>
<CheckIcon />
</IconButton>
<ConfirmDialog open={this.state.showAccept} onClose={() => this.setState({showAccept: false})} onOk={this.handleAccept.bind(this)}>
If you agree the selected uploads will move out of your private staging area into the public nomad.
</ConfirmDialog>
</FormGroup>
</div>
<div className={classes.uploads}>
{this.sortedUploads().map(upload => (
<Upload key={upload.gui_upload_id} upload={upload}
checked={selectedUploads.indexOf(upload) !== -1}
onDoesNotExist={() => this.handleDoesNotExist(upload)}
onCheckboxChanged={checked => this.onSelectionChanged(upload, checked)}/>
))}
</div>
</div>
)
} else {
return ''
}
const { selectedUploads } = this.state
const uploads = this.state.uploads || []
return (<div>
<div style={{width: '100%'}}>
<FormGroup className={classes.selectFormGroup} row>
<FormControlLabel label="all" style={{flexGrow: 1}} control={(
<Checkbox
checked={selectedUploads.length === uploads.length && uploads.length !== 0}
onChange={(_, checked) => this.onSelectionAllChanged(checked)}
/>
)} />
<IconButton onClick={() => this.update()}><ReloadIcon /></IconButton>
<FormLabel classes={{root: classes.selectLabel}}>
{`selected uploads ${selectedUploads.length}/${uploads.length}`}
</FormLabel>
<IconButton
disabled={selectedUploads.length === 0}
onClick={this.onDeleteClicked.bind(this)}
>
<DeleteIcon />
</IconButton>
<IconButton disabled={selectedUploads.length === 0} onClick={this.onAcceptClicked.bind(this)}>
<CheckIcon />
</IconButton>
<ConfirmDialog open={this.state.showAccept} onClose={() => this.setState({showAccept: false})} onOk={this.handleAccept.bind(this)}>
If you agree the selected uploads will move out of your private staging area into the public nomad.
</ConfirmDialog>
</FormGroup>
</div>
<div className={classes.uploads}>{
(uploads.length > 0)
? (
<div>
<Help cookie="uploadList">{`
These are all your existing not commiting uploads. You can see how processing
progresses and review your uploads before commiting them to the *nomad repository*.
Select uploads to delete or commit them. Click on uploads to see individual
calculations. Click on calculations to see more details on each calculation.
`}</Help>
{
this.sortedUploads().map(upload => (
<Upload key={upload.gui_upload_id} upload={upload}
checked={selectedUploads.indexOf(upload) !== -1}
onDoesNotExist={() => this.handleDoesNotExist(upload)}
onCheckboxChanged={checked => this.onSelectionChanged(upload, checked)}/>
))
}
</div>
) : ''
}</div>
</div>)
}
render() {
......@@ -214,12 +227,12 @@ class Uploads extends React.Component {
return (
<div className={classes.root}>
<Typography variant="h4">Upload your own data</Typography>
<Markdown>{`
<Help cookie="uploadHelp" component={Markdown}>{`
You can upload your own data. Have your code output ready in a popular archive
format (e.g. \`*.zip\` or \`*.tar.gz\`). Your upload can
comprise the output of multiple runs, even of different codes. Don't worry, nomad
will find it, just drop it below:`}
</Markdown>
will find it, just drop it below:
`}</Help>
<Paper className={classes.dropzoneContainer}>
<Dropzone
......@@ -234,16 +247,17 @@ class Uploads extends React.Component {
</Dropzone>
</Paper>
<Markdown>{`
<Help cookie="uploadCommandHelp">{`
Alternatively, you can upload files via the following shell command.
Replace \`<local_file>\` with your file. After executing the command,
return here and reload.
`}</Help>
<Markdown>{`
\`\`\`
${uploadCommand}
\`\`\`
`}
</Markdown>
`}</Markdown>
{this.renderUploads()}
{this.state.loading ? <LinearProgress/> : ''}
......
......@@ -173,6 +173,16 @@
version "3.1.3"
resolved "https://registry.yarnpkg.com/@navjobs/upload/-/upload-3.1.3.tgz#ea1160016b96b6a5e7139091fc7b53dcb4bbc6d4"
"@types/cookie@^0.3.1":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.2.tgz#453f4b14b25da6a8ea4494842dedcbf0151deef9"
"@types/hoist-non-react-statics@^3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.0.1.tgz#dde7c53101912dae8f45a1807f9857a59ddf3919"
dependencies:
"@types/react" "*"
"@types/jss@^9.5.6":
version "9.5.7"
resolved "https://registry.yarnpkg.com/@types/jss/-/jss-9.5.7.tgz#fa57a6d0b38a3abef8a425e3eb6a53495cb9d5a0"
......@@ -180,6 +190,10 @@
csstype "^2.0.0"
indefinite-observable "^1.0.1"
"@types/object-assign@^4.0.30":
version "4.0.30"
resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652"
"@types/prop-types@*":
version "15.5.8"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.5.8.tgz#8ae4e0ea205fe95c3901a5a1df7f66495e3a56ce"
......@@ -3666,6 +3680,12 @@ hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
hoist-non-react-statics@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b"
dependencies:
react-is "^16.7.0"
hoist-non-react-statics@^3.2.1:
version "3.2.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.2.1.tgz#c09c0555c84b38a7ede6912b61efddafd6e75e1e"
......@@ -6221,6 +6241,14 @@ react-base16-styling@^0.6.0:
lodash.flow "^3.3.0"
pure-color "^1.2.0"
react-cookie@^3.0.8:
version "3.0.8"
resolved "https://registry.yarnpkg.com/react-cookie/-/react-cookie-3.0.8.tgz#fd413d9940d5f2397700548e3d1faed0330e8bfd"
dependencies:
"@types/hoist-non-react-statics" "^3.0.1"
hoist-non-react-statics "^3.0.0"
universal-cookie "^3.0.7"
react-copy-to-clipboard@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.1.tgz#8eae107bb400be73132ed3b6a7b4fb156090208e"
......@@ -6285,7 +6313,7 @@ react-highlight@^0.12.0:
dependencies:
highlight.js "^9.11.0"
react-is@^16.3.2, react-is@^16.6.3:
react-is@^16.3.2, react-is@^16.6.3, react-is@^16.7.0:
version "16.7.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa"
......@@ -7634,6 +7662,15 @@ unique-string@^1.0.0:
dependencies:
crypto-random-string "^1.0.0"
universal-cookie@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-3.0.7.tgz#722e8bc455bb33ed3c74988344fad9d9bb88278e"
dependencies:
"@types/cookie" "^0.3.1"
"@types/object-assign" "^4.0.30"
cookie "^0.3.1"
object-assign "^4.1.0"
universalify@^0.1.0:
version "0.1.2"
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
......
Supports Markdown
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