diff --git a/client/job-view/job-view.js b/client/job-view/job-view.js
index 439a4684f..5b40f6505 100644
--- a/client/job-view/job-view.js
+++ b/client/job-view/job-view.js
@@ -46,10 +46,10 @@ module.exports = View.extend({
},
render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
- if(this.model.status !== "error") {
+ if(this.model.status === "complete") {
this.renderResultsView();
}
- if(!this.readOnly) {
+ if(!this.readOnly && this.model.status !== "running") {
this.renderLogsView();
}
this.renderSettingsView();
diff --git a/client/models/job.js b/client/models/job.js
index de495f6a2..622d2e8e0 100644
--- a/client/models/job.js
+++ b/client/models/job.js
@@ -74,6 +74,13 @@ module.exports = State.extend({
// timeZone = timeZone.replace('(', '').replace(')', '') // remove the '()' from the timezone
return stamp + hours + ":" + minutes + " " + ampm + " " + timeZone;
}
+ },
+ sortTime: {
+ deps: ["startTime"],
+ fn: function () {
+ let date = new Date(this.startTime);
+ return Math.round(date.getTime() / 1000);
+ }
}
}
});
\ No newline at end of file
diff --git a/client/models/jobs.js b/client/models/jobs.js
index c42797fb5..cba1f6543 100644
--- a/client/models/jobs.js
+++ b/client/models/jobs.js
@@ -22,5 +22,6 @@ let Collection = require('ampersand-collection');
let Job = require('./job');
module.exports = Collection.extend({
- model: Job
+ model: Job,
+ comparator: 'sortTime'
});
diff --git a/client/pages/workflow-manager.js b/client/pages/workflow-manager.js
index 3f8290a10..c86b176e0 100644
--- a/client/pages/workflow-manager.js
+++ b/client/pages/workflow-manager.js
@@ -49,12 +49,14 @@ let WorkflowManager = PageView.extend({
'click [data-target=start-job]' : 'clickStartJobHandler',
'click [data-hook=edit-model]' : 'clickEditModelHandler',
'click [data-hook=collapse-jobs]' : 'changeCollapseButtonText',
- 'click [data-hook=return-to-project-btn]' : 'handleReturnToProject'
+ 'click [data-hook=return-to-project-btn]' : 'handleReturnToProject',
+ 'click [data-hook=manual-view-control]' : 'setActiveJobViewControl'
},
initialize: function (attrs, options) {
PageView.prototype.initialize.apply(this, arguments);
let urlParams = new URLSearchParams(window.location.search);
let jobID = urlParams.has('job') ? urlParams.get('job') : null;
+ this.viewingActiveJob = false;
this.model = new Workflow({
directory: urlParams.get('path')
});
@@ -227,8 +229,9 @@ let WorkflowManager = PageView.extend({
this.jobListingView = this.renderCollection(
this.model.jobs,
JobListingView,
- this.queryByHook("job-listing")
+ this.queryByHook("job-listing"),
);
+ this.setActiveJobIndicator();
},
renderModelLocationSelectView: function (model) {
if(this.modelLocationSelectView) {
@@ -287,14 +290,13 @@ let WorkflowManager = PageView.extend({
}else if(this.model.activeJob.status !== "ready") {
this.renderStatusView();
}
- let detailsStatus = ["error", "complete"];
if(jobID !== null) {
let activeJob = this.model.jobs.filter((job) => { return job.name === jobID; })[0] || null;
if(activeJob !== null) {
this.model.activeJob = activeJob;
}
}
- if(this.model.activeJob && detailsStatus.includes(this.model.activeJob.status)) {
+ if(this.model.activeJob) {
this.renderActiveJob();
}
},
@@ -309,8 +311,20 @@ let WorkflowManager = PageView.extend({
setActiveJob: function (job) {
this.removeActiveJob();
this.model.activeJob = job;
+ this.viewingActiveJob = false;
+ this.setActiveJobViewControl();
this.renderActiveJob();
},
+ setActiveJobIndicator: function () {
+ this.jobListingView.views.forEach((view) => {
+ view.updateActiveJob(this.model.activeJob.directory);
+ });
+ },
+ setActiveJobViewControl: function (e) {
+ this.viewingActiveJob = !this.viewingActiveJob;
+ $(this.queryByHook("manual-view-control")).text(this.viewingActiveJob ? "Finished" : "View");
+ this.setActiveJobIndicator();
+ },
setupSettingsView: function () {
if(!this.model.newFormat) {
$(this.queryByHook("of-start-job")).css('display', 'inline-block');
@@ -375,31 +389,46 @@ let WorkflowManager = PageView.extend({
let runEndpoint = `${path.join(app.getApiPath(), "workflow/run-job")}${runQuery}`;
app.getXHR(runEndpoint, {
success: (err, response, body) => {
- this.updateWorkflow(true);
+ this.updateWorkflow({newJob: true});
}
});
}
});
},
- updateWorkflow: function (newJob) {
- let self = this;
+ updateWorkflow: function ({newJob=false}={}) {
if(this.model.newFormat) {
- let hadActiveJob = Boolean(this.model.activeJob.status)
app.getXHR(this.model.url(), {
- success: function (err, response, body) {
- self.model.set({jobs: body.jobs, activeJob: body.activeJob});
- if(!Boolean(self.model.activeJob.status)){
- self.removeActiveJob();
- }else if(!hadActiveJob && Boolean(self.model.activeJob.status)) {
- self.renderActiveJob();
+ success: (err, response, body) => {
+ this.model.set({jobs: body.jobs});
+ if(newJob || this.viewingActiveJob) {
+ setTimeout(() => { this.setActiveJobIndicator(); });
+ }else if (!Boolean(body.activeJob.status)) {
+ this.removeActiveJob();
+ }else {
+ this.model.set({activeJob: body.activeJob});
+ this.setActiveJobViewControl();
+ this.renderActiveJob();
+ }
+ if(newJob) {
+ setTimeout(() => { this.setActiveJobIndicator(); });
+ }else {
+ if(!Boolean(this.model.activeJob.status)) {
+ this.removeActiveJob();
+ }else if(!this.viewingActiveJob){
+ this.model.set({activeJob: body.activeJob});
+ this.renderActiveJob();
+ this.setActiveJobViewControl();
+ }else {
+ setTimeout(() => { this.setActiveJobIndicator(); });
+ }
}
}
});
}else if(!this.model.newFormat){
app.getXHR(this.model.url(), {
- success: function (err, response, body) {
- self.model.set(body)
- self.renderSubviews();
+ success: (err, response, body) => {
+ this.model.set(body)
+ this.renderSubviews();
}
});
}
diff --git a/client/settings-view/views/well-mixed-settings-view.js b/client/settings-view/views/well-mixed-settings-view.js
index 1f93e4ec8..89f1945a8 100644
--- a/client/settings-view/views/well-mixed-settings-view.js
+++ b/client/settings-view/views/well-mixed-settings-view.js
@@ -63,7 +63,8 @@ module.exports = View.extend({
}else {
if(!this.model.isAutomatic){
$(this.queryByHook('select-ode')).prop('checked', Boolean(this.model.algorithm === "ODE"));
- $(this.queryByHook('select-ssa')).prop('checked', Boolean(this.model.algorithm === "SSA"));
+ $(this.queryByHook('select-ssa')).prop('checked', Boolean(this.model.algorithm === "SSA"));
+ $(this.queryByHook('select-cle')).prop('checked', Boolean(this.model.algorithm === "CLE"));
$(this.queryByHook('select-tau-leaping')).prop('checked', Boolean(this.model.algorithm === "Tau-Leaping"));
$(this.queryByHook('select-hybrid-tau')).prop('checked', Boolean(this.model.algorithm === "Hybrid-Tau-Leaping"));
}else{
diff --git a/client/styles/styles.css b/client/styles/styles.css
index b0d830753..64d33801e 100644
--- a/client/styles/styles.css
+++ b/client/styles/styles.css
@@ -884,4 +884,19 @@ input:checked + .slider:before {
position: absolute;
left: 15px;
top: 40%;
-}
\ No newline at end of file
+}
+
+.active-job-listing {
+ background-color: rgba(0, 255, 255, 1.0);
+}
+
+.job-listing-btn {
+ color: rgb(108, 117, 125);
+ background-color: rgb(255, 255, 255) !important;
+}
+
+.job-listing-btn:hover {
+ color: rgb(255, 255, 255);
+ background-color: rgb(108, 117, 125) !important;
+}
+
diff --git a/client/templates/includes/jobListing.pug b/client/templates/includes/jobListing.pug
index c99fc0497..e8c95e9f2 100644
--- a/client/templates/includes/jobListing.pug
+++ b/client/templates/includes/jobListing.pug
@@ -3,24 +3,26 @@ div(data-hook=this.model.elementID)
if(this.model.collection.indexOf(this.model) !== 0)
hr
- div.row
+ div.py-1.pl-1(data-hook="is-active-job")
- div.col-md-3
+ div.row
- button.btn.btn-outline-secondary.box-shadow(data-hook=this.model.elementID + "-open" style="width: 100%")=this.model.name
+ div.col-md-3
- div.col-md-4
+ button.btn.btn-outline-secondary.box-shadow.job-listing-btn(data-hook=this.model.elementID + "-open" style="width: 100%")=this.model.name
- div.py-2=this.model.fmtStartTime
+ div.col-md-4
- div.col-md-3
+ div.py-2=this.model.fmtStartTime
- div.py-2
+ div.col-md-3
- div.inline(data-hook=this.model.elementID + "-status")=this.model.status
+ div.py-2
- div.inline.spinner-border.status(data-hook=this.model.elementID + "-running-spinner")
+ div.inline(data-hook=this.model.elementID + "-status")=this.model.status
- div.col-md-2
+ div.inline.spinner-border.status(data-hook=this.model.elementID + "-running-spinner")
- button.btn.btn-outline-secondary.box-shadow(data-hook=this.model.elementID + "-remove") X
\ No newline at end of file
+ div.col-md-2
+
+ button.btn.btn-outline-secondary.box-shadow.job-listing-btn(data-hook=this.model.elementID + "-remove") X
\ No newline at end of file
diff --git a/client/templates/pages/workflowManager.pug b/client/templates/pages/workflowManager.pug
index 05582cab7..bc9be6064 100644
--- a/client/templates/pages/workflowManager.pug
+++ b/client/templates/pages/workflowManager.pug
@@ -106,7 +106,13 @@ section.page
div.mt-4(data-hook="active-job-header-container" style="display: none")
- h2(data-hook="active-job-header") Job:
+ div.inline
+
+ h2(data-hook="active-job-header") Job:
+
+ div.mr-3.inline(style="float: right;")
+
+ button.btn.btn-outline-secondary.box-shadow(data-hook="manual-view-control") View
div(data-hook="active-job-container")
diff --git a/client/views/job-listing.js b/client/views/job-listing.js
index c561d46dd..5362476cf 100644
--- a/client/views/job-listing.js
+++ b/client/views/job-listing.js
@@ -36,11 +36,15 @@ module.exports = View.extend({
},
initialize: function (attrs, options) {
View.prototype.initialize.apply(this, arguments);
+ this.isActiveJob = false;
+ console.log(this.model.startTime, this.model.sortTime)
},
render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
+ if(this.isActiveJob) {
+ $(this.queryByHook("is-active-job")).addClass("active-job-listing");
+ }
if(this.model.status === "running") {
- $(this.queryByHook(this.model.elementID + "-open")).prop("disabled", true);
this.getJobStatus();
}else{
$(this.queryByHook(this.model.elementID + '-running-spinner')).css('display', "none")
@@ -80,5 +84,13 @@ module.exports = View.extend({
},
openActiveJob: function (e) {
this.parent.setActiveJob(this.model);
+ },
+ updateActiveJob: function (activeJob) {
+ this.isActiveJob = Boolean(activeJob) && activeJob === this.model.directory;
+ if(this.isActiveJob) {
+ $(this.queryByHook("is-active-job")).addClass("active-job-listing");
+ }else {
+ $(this.queryByHook("is-active-job")).removeClass("active-job-listing");
+ }
}
});
diff --git a/client/views/workflow-group-listing.js b/client/views/workflow-group-listing.js
index 23c15c9fd..cedb8aea2 100644
--- a/client/views/workflow-group-listing.js
+++ b/client/views/workflow-group-listing.js
@@ -44,12 +44,14 @@ module.exports = View.extend({
initialize: function (attrs, options) {
View.prototype.initialize.apply(this, arguments);
let links = [];
- this.model.model.refLinks.forEach((link) => {
- links.push(
- `${link.name}`
- );
- });
- this.htmlLinks = links.join('')
+ if(this.model.model.refLinks) {
+ this.model.model.refLinks.forEach((link) => {
+ links.push(
+ `${link.name}`
+ );
+ });
+ }
+ this.htmlLinks = links.join('');
},
render: function (attrs, options) {
View.prototype.render.apply(this, arguments);
diff --git a/jupyterhub/.env b/jupyterhub/.env
index 82ce710e2..d452a7505 100644
--- a/jupyterhub/.env
+++ b/jupyterhub/.env
@@ -5,7 +5,7 @@ JUPYTER_CONFIG_DIR=/opt/stochss-config/.jupyter
#AUTH_CLASS=jupyterhub.auth.DummyAuthenticator
-JUPYTERHUB_VERSION=1.1.0
+JUPYTERHUB_VERSION=3.1.1
DOCKER_HUB_IMAGE=stochss-hub
diff --git a/stochss/handlers/util/stochss_job.py b/stochss/handlers/util/stochss_job.py
index 6ba2add76..bd3e3605c 100644
--- a/stochss/handlers/util/stochss_job.py
+++ b/stochss/handlers/util/stochss_job.py
@@ -692,6 +692,18 @@ def get_time_stamp(self):
return time_stamp
return None
+ def get_update_time(self):
+ os.chdir(self.get_path(full=True))
+ if os.path.exists("COMPLETE"):
+ mod_time = os.path.getctime("COMPLETE")
+ elif os.path.exists("ERROR"):
+ mod_time = os.path.getctime("ERROR")
+ elif os.path.exists("RUNNING"):
+ mod_time = os.path.getctime("RUNNING")
+ else:
+ mod_time = None
+ os.chdir(self.user_dir)
+ return mod_time
def load(self, new=False):
'''
diff --git a/stochss/handlers/util/stochss_workflow.py b/stochss/handlers/util/stochss_workflow.py
index c687aeb08..2584790ad 100644
--- a/stochss/handlers/util/stochss_workflow.py
+++ b/stochss/handlers/util/stochss_workflow.py
@@ -165,20 +165,21 @@ def __load_annotation(self):
def __load_jobs(self):
self.workflow['jobs'] = []
- time = 0
+ mrm_time = 0
last_job = None
for file_obj in os.listdir(self.get_path(full=True)):
if file_obj.startswith("job_"):
path = os.path.join(self.path, file_obj)
- job = StochSSJob(path=path).load()
- self.workflow['jobs'].append(job)
- if os.path.getmtime(path) > time and job['status'] != "running":
- time = os.path.getmtime(path)
- last_job = job
+ job = StochSSJob(path=path)
+ self.workflow['jobs'].append(job.load())
+ cm_time = job.get_update_time()
+ if cm_time > mrm_time:
+ mrm_time = cm_time
+ last_job = len(self.workflow['jobs']) - 1
if last_job is None:
self.workflow['activeJob'] = None
else:
- self.workflow['activeJob'] = last_job
+ self.workflow['activeJob'] = self.workflow['jobs'][last_job]
def __load_settings(self):