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

Refactored api.js. Added basic login dialog.

parent 345878ef
import { UploadRequest } from '@navjobs/upload'
import Swagger from 'swagger-client'
import { apiBase } from './config'
const auth_headers = {
Authorization: 'Basic ' + btoa('sheldon.cooper@nomad-fairdi.tests.de:password')
}
const swaggerPromise = Swagger(`${apiBase}/swagger.json`, {
authorizations: {
// my_query_auth: new ApiKeyAuthorization('my-query', 'bar', 'query'),
// my_header_auth: new ApiKeyAuthorization('My-Header', 'bar', 'header'),
'HTTP Basic': {
username: 'sheldon.cooper@nomad-fairdi.tests.de',
password: 'password'
}
// cookie_: new CookieAuthorization('one=two')
}
})
export class DoesNotExist extends Error {
constructor(msg) {
super(msg)
this.name = 'DoesNotExist'
}
}
const handleApiError = (e) => {
if (e.response) {
const body = e.response.body
const message = (body && body.message) ? body.message : e.response.statusText
if (e.response.status === 404) {
throw new DoesNotExist(message)
} else {
throw Error(`API error (${e.response.status}): ${message}`)
}
} else {
throw Error('Network related error, cannot reach API: ' + e)
}
}
const upload_to_gui_ids = {}
let gui_upload_id_counter = 0
class Upload {
constructor(json) {
// Cannot use upload_id as key in GUI, because uploads don't have an upload_id
// before upload is completed
if (json.upload_id) {
// instance from the API
this.gui_upload_id = upload_to_gui_ids[json.upload_id]
if (this.gui_upload_id === undefined) {
// never seen in the GUI, needs a GUI id
this.gui_upload_id = gui_upload_id_counter++
upload_to_gui_ids[json.upload_id] = this.gui_upload_id
}
} else {
// new instance, not from the API
this.gui_upload_id = gui_upload_id_counter++
}
Object.assign(this, json)
}
uploadFile(file) {
const uploadFileWithProgress = async() => {
let uploadRequest = await UploadRequest(
{
request: {
url: `${apiBase}/uploads/?name=${this.name}`,
method: 'PUT',
headers: {
'Content-Type': 'application/gzip',
...auth_headers
}
},
files: [file],
progress: value => {
this.uploading = value
}
}
)
if (uploadRequest.error) {
handleApiError(uploadRequest.error)
}
if (uploadRequest.aborted) {
throw Error('User abort')
}
this.uploading = 100
this.upload_id = uploadRequest.response.upload_id
upload_to_gui_ids[this.upload_id] = this.gui_upload_id
}
return uploadFileWithProgress()
.then(() => this)
}
get(page, perPage, orderBy, order) {
if (this.uploading !== null && this.uploading !== 100) {
return new Promise(resolve => resolve(this))
} else {
if (this.upload_id) {
return swaggerPromise.then(client => client.apis.uploads.get_upload({
upload_id: this.upload_id,
page: page || 1,
per_page: perPage || 5,
order_by: orderBy || 'mainfile',
order: order || -1
}))
.catch(handleApiError)
.then(response => response.body)
.then(uploadJson => {
Object.assign(this, uploadJson)
return this
})
} else {
return new Promise(resolve => resolve(this))
}
}
}
}
function createUpload(name) {
return new Upload({
name: name,
tasks: ['uploading', 'extract', 'parse_all', 'cleanup'],
current_task: 'uploading',
uploading: 0,
create_time: new Date()
}, true)
}
async function getUploads() {
const client = await swaggerPromise
return client.apis.uploads.get_uploads()
.catch(handleApiError)
.then(response => response.body.map(uploadJson => {
const upload = new Upload(uploadJson)
upload.uploading = 100
return upload
}))
}
async function archive(uploadId, calcId) {
const client = await swaggerPromise
return client.apis.archive.get_archive_calc({
upload_id: uploadId,
calc_id: calcId
})
.catch(handleApiError)
.then(response => response.body)
}
async function calcProcLog(uploadId, calcId) {
const client = await swaggerPromise
return client.apis.archive.get_archive_logs({
upload_id: uploadId,
calc_id: calcId
})
.catch(handleApiError)
.then(response => response.text)
}
async function repo(uploadId, calcId) {
const client = await swaggerPromise
return client.apis.repo.get_repo_calc({
upload_id: uploadId,
calc_id: calcId
})
.catch(handleApiError)
.then(response => response.body)
}
async function repoAll(page, perPage, owner) {
const client = await swaggerPromise
return client.apis.repo.get_calcs({
page: page,
per_page: perPage,
ower: owner || 'all'
})
.catch(handleApiError)
.then(response => response.body)
}
async function deleteUpload(uploadId) {
const client = await swaggerPromise
return client.apis.uploads.delete_upload({upload_id: uploadId})
.catch(handleApiError)
.then(response => response.body)
}
async function commitUpload(uploadId) {
const client = await swaggerPromise
return client.apis.uploads.exec_upload_command({
upload_id: uploadId,
payload: {
command: 'commit'
}
})
.catch(handleApiError)
.then(response => response.body)
}
let cachedMetaInfo = null
async function getMetaInfo() {
if (cachedMetaInfo) {
return cachedMetaInfo
} else {
const loadMetaInfo = async(path) => {
const client = await swaggerPromise
return client.apis.archive.get_metainfo({metainfo_path: path})
.catch(handleApiError)
.then(response => response.body)
.then(data => {
if (!cachedMetaInfo) {
cachedMetaInfo = {
loadedDependencies: {}
}
}
cachedMetaInfo.loadedDependencies[path] = true
if (data.metaInfos) {
data.metaInfos.forEach(info => {
cachedMetaInfo[info.name] = info
info.relativePath = path
})
}
if (data.dependencies) {
data.dependencies
.filter(dep => cachedMetaInfo.loadedDependencies[dep.relativePath] !== true)
.forEach(dep => {
loadMetaInfo(dep.relativePath)
})
}
})
}
await loadMetaInfo('all.nomadmetainfo.json')
return cachedMetaInfo
}
}
async function getUploadCommand() {
const client = await swaggerPromise
return client.apis.uploads.get_upload_command()
.catch(handleApiError)
.then(response => response.body.upload_command)
}
const api = {
getUploadCommand: getUploadCommand,
createUpload: createUpload,
deleteUpload: deleteUpload,
commitUpload: commitUpload,
getUploads: getUploads,
archive: archive,
calcProcLog: calcProcLog,
repo: repo,
repoAll: repoAll,
getMetaInfo: getMetaInfo
}
export default api
......@@ -8,68 +8,36 @@ 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'
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') || []})
}
import { HelpProvider } from './help'
import { ApiProvider } from './api'
export default class App extends React.Component {
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>
<HelpProvider>
<ApiProvider>
<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>
</ApiProvider>
</HelpProvider>
</BrowserRouter>
</MuiThemeProvider>
)
}
}
export default withCookies(App)
......@@ -2,14 +2,15 @@ import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, LinearProgress } from '@material-ui/core'
import ReactJson from 'react-json-view'
import api from '../api'
import { compose } from 'recompose'
import { withErrors } from './errors'
import Markdown from './Markdown'
import { withApi } from './api'
class ArchiveCalcView extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired
......@@ -41,7 +42,7 @@ class ArchiveCalcView extends React.Component {
}
componentDidMount() {
const {uploadId, calcId} = this.props
const {uploadId, calcId, api} = this.props
api.archive(uploadId, calcId).then(data => {
this.setState({data: data})
}).catch(error => {
......@@ -94,4 +95,4 @@ class ArchiveCalcView extends React.Component {
}
}
export default compose(withErrors, withStyles(ArchiveCalcView.styles))(ArchiveCalcView)
export default compose(withApi(false), withErrors, withStyles(ArchiveCalcView.styles))(ArchiveCalcView)
import React from 'react'
import PropTypes from 'prop-types'
import { withStyles, LinearProgress } from '@material-ui/core'
import api from '../api'
import { compose } from 'recompose'
import { withErrors } from './errors'
import { withApi } from './api'
class ArchiveLogView extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired
......@@ -25,12 +26,12 @@ class ArchiveLogView extends React.Component {
}
componentDidMount() {
const {uploadId, calcId} = this.props
const {uploadId, calcId, api, raiseError} = this.props
api.calcProcLog(uploadId, calcId).then(data => {
this.setState({data: data})
}).catch(error => {
this.setState({data: null})
this.props.raiseError(error)
raiseError(error)
})
}
......@@ -49,4 +50,4 @@ class ArchiveLogView extends React.Component {
}
}
export default compose(withErrors, withStyles(ArchiveLogView.styles))(ArchiveLogView)
export default compose(withApi(false), withErrors, withStyles(ArchiveLogView.styles))(ArchiveLogView)
......@@ -3,11 +3,12 @@ import PropTypes from 'prop-types'
import { withStyles, Dialog, DialogContent, DialogActions, Button, DialogTitle, Tab, Tabs,
Typography, FormGroup, FormControlLabel, Checkbox, Divider, FormLabel, IconButton,
LinearProgress } from '@material-ui/core'
import api from '../api'
import SwipeableViews from 'react-swipeable-views'
import ArchiveCalcView from './ArchiveCalcView'
import DownloadIcon from '@material-ui/icons/CloudDownload'
import ArchiveLogView from './ArchiveLogView'
import { withApi } from './api'
import { compose } from 'recompose'
function CalcQuantity(props) {
const {children, label, typography} = props
......@@ -56,6 +57,7 @@ class CalcDialog extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
raiseError: PropTypes.func.isRequired,
uploadId: PropTypes.string.isRequired,
calcId: PropTypes.string.isRequired,
......@@ -70,7 +72,7 @@ class CalcDialog extends React.Component {
componentDidMount() {
const {uploadId, calcId} = this.props
api.repo(uploadId, calcId).then(data => {
this.props.api.repo(uploadId, calcId).then(data => {
this.setState({calcData: data})
}).catch(error => {
this.setState({calcData: null})
......@@ -210,4 +212,4 @@ class CalcDialog extends React.Component {
}
}
export default withStyles(CalcDialog.styles)(CalcDialog)
export default compose(withApi(false), withStyles(CalcDialog.styles))(CalcDialog)
import React from 'react'
import { withStyles, Button } from '@material-ui/core'
import Markdown from './Markdown'
import PropTypes from 'prop-types'
import PropTypes, { instanceOf } from 'prop-types'
import { Cookies, withCookies } from 'react-cookie'
export class HelpManager {
isOpen(helpKey) {
return true
export const HelpContext = React.createContext()
class HelpProviderComponent extends React.Component {
static propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
cookies: instanceOf(Cookies).isRequired
}
gotIt(helpKey) {
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)
}
}
}
export const HelpContext = React.createContext(new HelpManager())
componentDidMount() {
this.setState({helpCookies: this.props.cookies.get('help') || []})
}
render() {
return (
<HelpContext.Provider value={this.state}>
{this.props.children}
</HelpContext.Provider>
)
}
}
class HelpComponent extends React.Component {
static styles = theme => ({
......@@ -64,4 +101,5 @@ class HelpComponent extends React.Component {
}
}
export const HelpProvider = withCookies(HelpProviderComponent)
export const Help = withStyles(HelpComponent.styles)(HelpComponent)
......@@ -23,11 +23,12 @@ 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 { MuiThemeProvider, IconButton, Button, Checkbox, FormLabel } from '@material-ui/core'
import { MuiThemeProvider, IconButton, Button, Checkbox, FormLabel, DialogTitle, DialogContent, DialogContentText, TextField, DialogActions, Dialog } from '@material-ui/core'
import { genTheme, repoTheme, archiveTheme, encTheme, analyticsTheme } from '../config'
import { ErrorSnacks } from './errors'
import classNames from 'classnames'
import { HelpContext } from './Help'
import { HelpContext } from './help'
import { withApi } from './api'
const drawerWidth = 200
......@@ -55,6 +56,122 @@ const toolbarThemes = {
'/dev': genTheme
}
class LoginLogoutComponent extends React.Component {
static propTypes = {
classes: PropTypes.object.isRequired,
api: PropTypes.object.isRequired,
userName: PropTypes.string,
login: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired
}
static styles = theme => ({
root: {
display: 'flex',
alignItems: 'center',
'& p': {
marginRight: theme.spacing.unit * 2
},
'& button': {
borderColor: theme.palette.getContrastText(theme.palette.primary.main),
marginRight: theme.spacing.unit * 4
}
}
})
constructor(props) {
super(props)
this.handleLoginDialogClosed = this.handleLoginDialogClosed.bind(this)
this.handleLogout = this.handleLogout.bind(this)
this.handleChange = this.handleChange.bind(this)
}
state = {
loginDialogOpen: false,
userName: null,
password: null
}
handleLoginDialogClosed(withLogin) {
this.setState({loginDialogOpen: false})
if (withLogin) {
this.props.login(this.state.userName, this.state.password)
}
}
handleChange = name => event => {
this.setState({
[name]: event.target.value
})
}
handleLogout() {
this.props.logout()
}
render() {
const { classes, userName } = this.props
if (userName) {
return (