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):