Commit 2280e95c authored by Markus Scheidgen's avatar Markus Scheidgen
Browse files

Changed the gui to reflect delete and commit running as processes. Overhawled...

Changed the gui to reflect delete and commit running as processes. Overhawled gui API error handling. Improved the upload view.
parent 3f79b6e1
Pipeline #42047 passed with stages
in 16 minutes and 59 seconds
No preview for this file type
......@@ -35,6 +35,7 @@
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-node": "^8.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-react": "^7.11.1",
"eslint-plugin-standard": "^3.1.0",
......
......@@ -18,31 +18,47 @@ const swaggerPromise = Swagger(`${apiBase}/swagger.json`, {
}
})
const networkError = (e) => {
console.log(e)
throw Error('Network related error, cannot reach API: ' + e)
}
const handleJsonErrors = (e) => {
console.log(e)
throw Error('API return unexpected data format.')
export class DoesNotExist extends Error {
constructor(msg) {
super(msg)
this.name = 'DoesNotExist'
}
}
const handleResponseErrors = (response) => {
if (!response.ok) {
return response.json()
.catch(() => {
throw Error(`API error (${response.status}): ${response.statusText}`)
}).then(data => {
throw Error(`API error (${response.status}): ${data.message}`)
})
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)
}
return response
}
const upload_to_gui_ids = {}
let gui_upload_id_counter = 0
class Upload {
constructor(json) {
this.uploading = 0
// 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
console.log('new gui ui')
}
} else {
// new instance, not from the API
this.gui_upload_id = gui_upload_id_counter++
}
Object.assign(this, json)
}
......@@ -65,13 +81,14 @@ class Upload {
}
)
if (uploadRequest.error) {
networkError(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()
......@@ -84,14 +101,13 @@ class Upload {
} 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(networkError)
.then(handleResponseErrors)
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)
......@@ -107,7 +123,7 @@ class Upload {
function createUpload(name) {
return new Upload({
name: name,
tasks: ['uploading'],
tasks: ['uploading', 'extract', 'parse_all', 'cleanup'],
current_task: 'uploading',
uploading: 0,
create_time: new Date()
......@@ -117,8 +133,7 @@ function createUpload(name) {
async function getUploads() {
const client = await swaggerPromise
return client.apis.uploads.get_uploads()
.catch(networkError)
.then(handleResponseErrors)
.catch(handleApiError)
.then(response => response.body.map(uploadJson => {
const upload = new Upload(uploadJson)
upload.uploading = 100
......@@ -129,11 +144,10 @@ async function getUploads() {
async function archive(uploadId, calcId) {
const client = await swaggerPromise
return client.apis.archive.get_archive_calc({
upload_id: uploadId,
calc_id: calcId
})
.catch(networkError)
.then(handleResponseErrors)
upload_id: uploadId,
calc_id: calcId
})
.catch(handleApiError)
.then(response => response.body)
}
......@@ -143,61 +157,47 @@ async function calcProcLog(uploadId, calcId) {
upload_id: uploadId,
calc_id: calcId
})
.catch(networkError)
.then(response => {
if (!response.ok) {
if (response.status === 404) {
return ''
} else {
return handleResponseErrors(response)
}
} else {
return response.text
}
})
.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(networkError)
.then(handleResponseErrors)
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(networkError)
.then(handleResponseErrors)
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(networkError)
.then(handleResponseErrors)
.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: {
operation: 'commit'
}
})
.catch(networkError)
.then(handleResponseErrors)
upload_id: uploadId,
payload: {
operation: 'commit'
}
})
.catch(handleApiError)
.then(response => response.body)
}
......@@ -208,11 +208,11 @@ async function getMetaInfo() {
return cachedMetaInfo
} else {
const loadMetaInfo = async(path) => {
return fetch(`${apiBase}/archive/metainfo/${path}`)
.catch(networkError)
.then(handleResponseErrors)
.then(response => response.json())
.catch(handleJsonErrors)
const client = await swaggerPromise
console.log(path)
return client.apis.archive.get_metainfo({metainfo_path: path})
.catch(handleApiError)
.then(response => response.body)
.then(data => {
if (!cachedMetaInfo) {
cachedMetaInfo = {
......@@ -243,8 +243,7 @@ async function getMetaInfo() {
async function getUploadCommand() {
const client = await swaggerPromise
return client.apis.uploads.get_upload_command()
.catch(networkError)
.then(handleResponseErrors)
.catch(handleApiError)
.then(response => response.body.upload_command)
}
......
......@@ -20,7 +20,8 @@ class Upload extends React.Component {
raiseError: PropTypes.func.isRequired,
upload: PropTypes.object.isRequired,
checked: PropTypes.bool,
onCheckboxChanged: PropTypes.func
onCheckboxChanged: PropTypes.func,
onDoesNotExist: PropTypes.func
}
static styles = theme => ({
......@@ -77,7 +78,7 @@ class Upload extends React.Component {
params: {
page: 1,
perPage: 5,
orderBy: 'status',
orderBy: 'tasks_status',
order: 'asc'
},
archiveLogs: null, // { uploadId, calcId } ids of archive to show logs for
......@@ -96,8 +97,9 @@ class Upload extends React.Component {
this.setState({loading: true})
this.state.upload.get(page, perPage, orderBy, order === 'asc' ? 1 : -1)
.then(upload => {
const {tasks_running, process_running, current_task} = upload
if (!this._unmounted) {
const continueUpdating = upload.status !== 'SUCCESS' && upload.status !== 'FAILURE' && !upload.is_stale
const continueUpdating = tasks_running || process_running || current_task === 'uploading'
this.setState({upload: upload, loading: false, params: params, updating: continueUpdating})
if (continueUpdating) {
window.setTimeout(() => {
......@@ -111,7 +113,11 @@ class Upload extends React.Component {
.catch(error => {
if (!this._unmounted) {
this.setState({loading: false, ...params})
this.props.raiseError(error)
if (error.name === 'DoesNotExist') {
this.props.onDoesNotExist()
} else {
this.props.raiseError(error)
}
}
})
}
......@@ -120,6 +126,12 @@ class Upload extends React.Component {
this.update(this.state.params)
}
componentDidUpdate(prevProps) {
if (!prevProps.upload.process_running && this.props.upload.process_running) {
this.update(this.state.params)
}
}
componentWillUnmount() {
this._unmounted = true
}
......@@ -169,73 +181,119 @@ class Upload extends React.Component {
renderStepper() {
const { classes } = this.props
const { upload } = this.state
const { calcs, tasks, current_task, status, errors } = upload
let activeStep = tasks.indexOf(current_task)
activeStep += (status === 'SUCCESS') ? 1 : 0
const { calcs, tasks, current_task, tasks_running, tasks_status, process_running, current_process, errors } = upload
// map tasks [ uploading, extracting, parse_all, cleanup ] to steps
const steps = [ 'upload', 'process', 'commit' ]
let step = null
const task_index = tasks.indexOf(current_task)
if (task_index === 0) {
step = 'upload'
} else if (task_index > 0 && tasks_running) {
step = 'process'
} else {
step = 'commit'
}
const stepIndex = steps.indexOf(step)
const labelPropsFactories = {
uploading: (props) => {
props.children = 'uploading'
const { uploading } = upload
if (upload.status !== 'FAILURE') {
props.optional = (
<Typography variant="caption">
{uploading === 100 && current_task === tasks[0] ? 'waiting for processing ...' : `${uploading || 0}%`}
</Typography>
)
upload: (props) => {
if (step === 'upload') {
props.children = 'uploading'
const { uploading } = upload
if (upload.tasks_status !== 'FAILURE') {
props.optional = (
<Typography variant="caption">
{`${uploading || 0}%`}
</Typography>
)
}
} else {
props.children = 'uploaded'
}
},
extracting: (props) => {
props.children = 'extracting'
process: (props) => {
props.error = tasks_status === 'FAILURE'
const processIndex = steps.indexOf('process')
if (stepIndex <= processIndex) {
props.children = 'processing'
} else {
props.children = 'processed'
}
if (current_task === 'extracting') {
props.children = 'extracting'
props.optional = (
<Typography variant="caption">
be patient
</Typography>
)
} else if (current_task === 'parse_all') {
props.children = 'parsing'
}
},
parse_all: (props) => {
props.children = 'parse'
if (!calcs) {
props.optional = (
<Typography variant="caption" >
loading...
</Typography>
)
} else if (calcs.pagination.total > 0) {
const { total, successes, failures } = calcs.pagination
if (failures) {
props.error = true
if (stepIndex >= processIndex) {
if (!calcs) {
props.optional = (
<Typography variant="caption" color="error">
{successes + failures}/{total}, {failures} failed
<Typography variant="caption" >
matching...
</Typography>
)
} else {
} else if (calcs.pagination.total > 0) {
const { total, successes, failures } = calcs.pagination
if (failures) {
props.error = true
props.optional = (
<Typography variant="caption" color="error">
{successes + failures}/{total}, {failures} failed
</Typography>
)
} else {
props.optional = (
<Typography variant="caption">
{successes + failures}/{total}
</Typography>
)
}
} else if (tasks_status === 'SUCCESS') {
props.error = true
props.optional = (
<Typography variant="caption">
{successes + failures}/{total}
</Typography>
<Typography variant="caption" color="error">No calculations found.</Typography>
)
}
} else if (status === 'SUCCESS') {
props.error = true
}
if (tasks_status === 'FAILURE') {
props.optional = (
<Typography variant="caption" color="error">No calculations found.</Typography>
<Typography variant="caption" color="error">
{errors.join(' ')}
</Typography>
)
}
},
commit: (props) => {
props.children = 'inspect'
if (process_running) {
if (current_process === 'commit_upload') {
props.children = 'approved'
props.optional = <Typography variant="caption">moving data ...</Typography>
} else if (current_process === 'delete_upload') {
props.children = 'declined'
props.optional = <Typography variant="caption">deleting data ...</Typography>
}
} else {
props.optional = <Typography variant="caption">commit or delete</Typography>
}
}
}
return (
<Stepper activeStep={activeStep} classes={{root: classes.stepper}}>
{tasks.map((label, index) => {
<Stepper activeStep={steps.indexOf(step)} classes={{root: classes.stepper}}>
{steps.map((label, index) => {
const labelProps = {
children: label,
error: activeStep === index && status === 'FAILURE'
children: label
}
const labelPropsFactory = labelPropsFactories[label]
......@@ -243,14 +301,6 @@ class Upload extends React.Component {
labelPropsFactory(labelProps)
}
if (labelProps.error && status === 'FAILURE') {
labelProps.optional = (
<Typography variant="caption" color="error">
{errors.join(' ')}
</Typography>
)
}
return (
<Step key={label}>
<StepLabel {...labelProps} />
......@@ -264,14 +314,14 @@ class Upload extends React.Component {
renderCalcTable() {
const { classes } = this.props
const { page, perPage, orderBy, order } = this.state.params
const { calcs, status, waiting } = this.state.upload
const { calcs, tasks_status, waiting } = this.state.upload
const { pagination, results } = calcs
if (pagination.total === 0) {
if (this.state.upload.completed) {
if (!this.state.upload.tasks_running) {
return (
<Typography className={classes.detailsContent}>
{status === 'SUCCESS' ? 'No calculcations found.' : 'No calculations to show.'}
{tasks_status === 'SUCCESS' ? 'No calculcations found.' : 'No calculations to show.'}
</Typography>
)
} else {
......@@ -292,8 +342,8 @@ class Upload extends React.Component {
}
const renderRow = (calc, index) => {
const { mainfile, calc_id, upload_id, parser, tasks, current_task, status, errors } = calc
const color = status === 'FAILURE' ? 'error' : 'default'
const { mainfile, calc_id, upload_id, parser, tasks, current_task, tasks_status, errors } = calc
const color = tasks_status === 'FAILURE' ? 'error' : 'default'
const row = (
<TableRow key={index}>
<TableCell>
......@@ -322,22 +372,20 @@ class Upload extends React.Component {
</TableCell>
<TableCell>
<Typography color={color}>
{(status === 'SUCCESS' || status === 'FAILURE')
?
<a className={classes.logLink} href="#logs" onClick={() => this.setState({archiveLogs: { uploadId: upload_id, calcId: calc_id }})}>
{status.toLowerCase()}
</a>
: status.toLowerCase()
{(tasks_status === 'SUCCESS' || tasks_status === 'FAILURE')
? <a className={classes.logLink} href="#logs" onClick={() => this.setState({archiveLogs: { uploadId: upload_id, calcId: calc_id }})}>
{tasks_status.toLowerCase()}
</a> : tasks_status.toLowerCase()
}
</Typography>
</TableCell>
<TableCell>
<CalcLinks uploadId={upload_id} calcId={calc_id} disabled={status !== 'SUCCESS'} />
<CalcLinks uploadId={upload_id} calcId={calc_id} disabled={tasks_status !== 'SUCCESS'} />
</TableCell>
</TableRow>
)
if (status === 'FAILURE') {
if (tasks_status === 'FAILURE') {
return (
<Tooltip key={calc_id} title={errors.map((error, index) => (<p key={`${calc_id}-${index}`}>{error}</p>))}>
{row}
......@@ -355,7 +403,7 @@ class Upload extends React.Component {
{ id: 'mainfile', sort: true, label: 'mainfile' },
{ id: 'parser', sort: true, label: 'code' },
{ id: 'task', sort: false, label: 'task' },
{ id: 'status', sort: true, label: 'status' },
{ id: 'tasks_status', sort: true, label: 'status' },
{ id: 'links', sort: false, label: 'links' }
]
......@@ -434,7 +482,7 @@ class Upload extends React.Component {
<ExpansionPanel>
<ExpansionPanelSummary
expandIcon={<ExpandMoreIcon/>} classes={{root: classes.summary}}>
{!(upload.completed || upload.waiting)
{(upload.tasks_running || upload.process_running)
? <div className={classes.progress}>
<CircularProgress size={32}/>
</div>
......
......@@ -119,6 +119,17 @@ class Uploads extends React.Component {
})
}
sortedUploads() {
return this.state.uploads.concat()
.sort((a, b) => (a.gui_upload_id === b.gui_upload_id) ? 0 : ((a.gui_upload_id < b.gui_upload_id) ? -1 : 1))
}
handleDoesNotExist(nonExistingUupload) {
this.setState({
uploads: this.state.uploads.filter(upload => upload !== nonExistingUupload)
})
}
onDrop(files) {
files.forEach(file => {
const upload = api.createUpload(file.name)
......@@ -139,7 +150,7 @@ class Uploads extends React.Component {
onSelectionAllChanged(checked) {
if (checked) {
this.setState({selectedUploads: [...this.state.uploads.filter(upload => upload.completed)]})
this.setState({selectedUploads: [...this.state.uploads.filter(upload => !upload.tasks_running)]})
} else {
this.setState({selectedUploads: []})
}
......@@ -181,9 +192,10 @@ class Uploads extends React.Component {
</FormGroup>
</div>
<div className={classes.uploads}>
{this.state.uploads.map((upload) => (
<Upload key={upload.upload_id} upload={upload}
{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>
......