diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index 90c1f153bcf8..188176b812e4 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -384,7 +384,12 @@ qx.Class.define("osparc.dashboard.CardBase", { }, uiMode: { - check: ["workbench", "guided", "app", "standalone"], // "guided" is no longer used + check: [ + "workbench", // =auto, the frontend decides the icon and default view + "app", "guided", // "guided" is no longer used + "standalone", + "pipeline", + ], nullable: true, apply: "__applyUiMode" }, diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 6d671f575549..d4bc4d3c1ec6 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -481,7 +481,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { if ( osparc.utils.Resources.isService(resourceData) || !osparc.product.Utils.showStudyPreview() || - !(osparc.study.Utils.getUiMode(resourceData) === "workbench") + ["app", "guided", "standalone"].includes(osparc.study.Utils.getUiMode(resourceData)) ) { // there is no pipelining or don't show it return null; diff --git a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js index fc877e620cae..941c02a1e230 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js @@ -1789,17 +1789,52 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { convertToPipelineButton["convertToPipelineButton"] = true; const uiMode = osparc.study.Utils.getUiMode(studyData); convertToPipelineButton.setVisibility(uiMode === "standalone" ? "visible" : "excluded"); - convertToPipelineButton.addListener("execute", () => { - this.__updateUIMode(studyData, "workbench") - .catch(err => osparc.FlashMessenger.logError(err, this.tr("Something went wrong while converting to pipeline"))); - }, this); + convertToPipelineButton.addListener("execute", () => this.__convertToPipelineClicked(studyData), this); return convertToPipelineButton; }, + __convertToPipelineClicked: function(studyData) { + let message = this.tr("Would you like to convert this project to a pipeline?"); + message += "
" + this.tr("Alternatively, you can create a copy of the project and convert the copy instead."); + const confirmationWin = new osparc.ui.window.Confirmation(); + confirmationWin.set({ + caption: this.tr("Convert to Pipeline"), + confirmText: this.tr("Convert"), + confirmAction: "create", + message, + }); + confirmationWin.getChildControl("cancel-button").exclude(); + const copyOptionButton = new qx.ui.form.Button().set({ + appearance: "form-button-text", + label: this.tr("Create a copy and convert it"), + }); + confirmationWin.getChildControl("buttons-layout").addAt(copyOptionButton, 0); + confirmationWin.addListener("close", () => { + if (confirmationWin.getConfirmed()) { + this.__updateUIMode(studyData, "pipeline") + .then(() => osparc.FlashMessenger.logAs(this.tr("Project converted to pipeline"), "INFO")) + .catch(err => osparc.FlashMessenger.logError(err, this.tr("Something went wrong while converting to pipeline"))); + } + }); + copyOptionButton.addListener("execute", () => { + confirmationWin.close(); + this.__duplicateStudy(studyData) + .then(task => { + task.addListener("resultReceived", e => { + const copiedStudy = e.getData(); + this.__updateUIMode(copiedStudy, "pipeline") + .then(() => osparc.FlashMessenger.logAs(this.tr("Project's copy converted to pipeline"), "INFO")) + .catch(err => osparc.FlashMessenger.logError(err, this.tr("Something went wrong while converting the copy to pipeline"))); + }, this); + }); + }, this); + confirmationWin.open(); + }, + __updateUIMode: function(studyData, uiMode) { const studyUI = osparc.utils.Utils.deepCloneObject(studyData["ui"]); studyUI["mode"] = uiMode; - return osparc.info.StudyUtils.patchStudyData(studyData, "ui", studyUI) + return osparc.store.Study.patchStudyData(studyData, "ui", studyUI) .then(() => this._updateStudyData(studyData)) }, @@ -1889,21 +1924,11 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }, __duplicateStudy: function(studyData) { - const text = this.tr("Duplicate process started and added to the background tasks"); - osparc.FlashMessenger.logAs(text, "INFO"); - - const params = { - url: { - "studyId": studyData["uuid"] - } - }; - const options = { - pollTask: true - }; - const fetchPromise = osparc.data.Resources.fetch("studies", "duplicate", params, options); - const pollTasks = osparc.store.PollTasks.getInstance(); - pollTasks.createPollingTask(fetchPromise) - .then(task => this.__taskDuplicateReceived(task, studyData["name"])) + osparc.study.Utils.duplicateStudy(studyData) + .then(task => { + this.__taskDuplicateReceived(task, studyData["name"]); + return task; + }) .catch(err => osparc.FlashMessenger.logError(err, this.tr("Something went wrong while duplicating"))); }, diff --git a/services/static-webserver/client/source/class/osparc/data/PollTask.js b/services/static-webserver/client/source/class/osparc/data/PollTask.js index ffa82e59d7b6..65c08968111e 100644 --- a/services/static-webserver/client/source/class/osparc/data/PollTask.js +++ b/services/static-webserver/client/source/class/osparc/data/PollTask.js @@ -102,9 +102,13 @@ qx.Class.define("osparc.data.PollTask", { }, extractProgress: function(updateData) { + const toNumberWithMaxTwoDecimals = value => { + return Math.round(Number(value) * 100) / 100; + }; + if ("task_progress" in updateData) { const taskProgress = updateData["task_progress"]; - const percent = taskProgress["percent"] ? parseFloat(taskProgress["percent"].toFixed(3)) : taskProgress["percent"]; + const percent = taskProgress["percent"] ? toNumberWithMaxTwoDecimals(taskProgress["percent"]) : taskProgress["percent"]; return percent; } return 0; diff --git a/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js b/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js index fcc2f7bd9f84..ea6aa4b58286 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js +++ b/services/static-webserver/client/source/class/osparc/data/model/StudyUI.js @@ -63,7 +63,12 @@ qx.Class.define("osparc.data.model.StudyUI", { }, mode: { - check: ["workbench", "guided", "app", "standalone"], // "guided" is no longer used + check: [ + "workbench", // =auto, the frontend decides the icon and default view + "app", "guided", // "guided" is no longer used + "standalone", + "pipeline", + ], init: "workbench", nullable: true, event: "changeMode", diff --git a/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js b/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js index 52c33d94be98..007038747383 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js @@ -72,7 +72,7 @@ qx.Class.define("osparc.desktop.SlideshowView", { const nodeId = e.getData(); this.__hideNode(nodeId); }, this); - slideshowToolbar.addListener("slidesStop", () => this.getStudy().getUi().setMode("workbench"), this); + slideshowToolbar.addListener("slidesStop", () => this.getStudy().getUi().setMode("pipeline"), this); this._add(slideshowToolbar); const mainView = this.__mainView = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ diff --git a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js index 1f87846ccc90..eaae2e78fb35 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -707,14 +707,19 @@ qx.Class.define("osparc.desktop.StudyEditor", { this.__viewsStack.setSelection([this.__slideshowView]); this.__slideshowView.startSlides(); break; - case "standalone": { + case "standalone": this.__viewsStack.setSelection([this.__workbenchView]); this.__workbenchView.openFirstNode(); break; - } + case "pipeline": + this.__viewsStack.setSelection([this.__workbenchView]); + this.__workbenchView.setMaximized(false); + this.__workbenchView.showPipeline(); + break; case "workbench": default: { this.__viewsStack.setSelection([this.__workbenchView]); + // OM: Is this needed? if (oldUIMode === "standalone") { // in this transition, show workbenchUI this.__workbenchView.setMaximized(false); diff --git a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js index 2f84aedf3635..d0bf34fb2951 100644 --- a/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js +++ b/services/static-webserver/client/source/class/osparc/jobs/JobsButton.js @@ -72,7 +72,7 @@ qx.Class.define("osparc.jobs.JobsButton", { }, __updateJobsButton: function() { - this._createChildControlImpl("icon"); + this.getChildControl("icon"); const number = this.getChildControl("number"); const jobsStore = osparc.store.Jobs.getInstance(); diff --git a/services/static-webserver/client/source/class/osparc/navigation/StudyTitleWOptions.js b/services/static-webserver/client/source/class/osparc/navigation/StudyTitleWOptions.js index 4c9c4cff32e7..92fa0cf3f7f0 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/StudyTitleWOptions.js +++ b/services/static-webserver/client/source/class/osparc/navigation/StudyTitleWOptions.js @@ -80,18 +80,7 @@ qx.Class.define("osparc.navigation.StudyTitleWOptions", { label: this.tr("Convert to Pipeline"), icon: null, }); - control.addListener("execute", () => { - this.getStudy().getUi().setMode("workbench"); - }); - break; - case "study-menu-convert-to-standalone": - control = new qx.ui.menu.Button().set({ - label: this.tr("Convert to Standalone"), - icon: null, - }); - control.addListener("execute", () => { - this.getStudy().getUi().setMode("standalone"); - }); + control.addListener("execute", () => this.__convertToPipelineClicked(), this); break; case "study-menu-restore": control = new qx.ui.menu.Button().set({ @@ -114,8 +103,9 @@ qx.Class.define("osparc.navigation.StudyTitleWOptions", { optionsMenu.setAppearance("menu-wider"); optionsMenu.add(this.getChildControl("study-menu-info")); optionsMenu.add(this.getChildControl("study-menu-reload")); - optionsMenu.add(this.getChildControl("study-menu-convert-to-pipeline")); - optionsMenu.add(this.getChildControl("study-menu-convert-to-standalone")); + if (osparc.product.Utils.hasConvertToPipelineEnabled()) { + optionsMenu.add(this.getChildControl("study-menu-convert-to-pipeline")); + } optionsMenu.add(this.getChildControl("study-menu-restore")); optionsMenu.add(this.getChildControl("study-menu-open-logger")); control = new qx.ui.form.MenuButton().set({ @@ -161,22 +151,11 @@ qx.Class.define("osparc.navigation.StudyTitleWOptions", { converter: mode => mode === "standalone" ? "visible" : "excluded" }); - const convertToPipelineButton = this.getChildControl("study-menu-convert-to-pipeline"); - const convertToStandaloneButton = this.getChildControl("study-menu-convert-to-standalone"); if (osparc.product.Utils.hasConvertToPipelineEnabled()) { + const convertToPipelineButton = this.getChildControl("study-menu-convert-to-pipeline"); study.getUi().bind("mode", convertToPipelineButton, "visibility", { converter: mode => mode === "standalone" ? "visible" : "excluded" }); - - const evaluateConvertToStandaloneButton = () => { - // exclude until we have the export to standalone backend functionality - convertToStandaloneButton.exclude(); - }; - study.getWorkbench().addListener("pipelineChanged", () => evaluateConvertToStandaloneButton()); - study.getUi().addListener("changeMode", () => evaluateConvertToStandaloneButton()); - } else { - convertToPipelineButton.exclude(); - convertToStandaloneButton.exclude(); } const restoreButton = this.getChildControl("study-menu-restore"); @@ -191,6 +170,25 @@ qx.Class.define("osparc.navigation.StudyTitleWOptions", { } else { this.exclude(); } - } + }, + + __convertToPipelineClicked: function() { + let message = this.tr("Would you like to convert this project to a pipeline?"); + message += "
" + this.tr("If you want to create a copy of the project and convert the copy instead, please close the project first."); + const confirmationWin = new osparc.ui.window.Confirmation(); + confirmationWin.set({ + caption: this.tr("Convert to Pipeline"), + confirmText: this.tr("Convert"), + confirmAction: "create", + message, + }); + confirmationWin.addListener("close", () => { + if (confirmationWin.getConfirmed()) { + this.getStudy().getUi().setMode("pipeline"); + osparc.FlashMessenger.logAs(this.tr("Project converted to pipeline"), "INFO"); + } + }); + confirmationWin.open(); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/product/Utils.js b/services/static-webserver/client/source/class/osparc/product/Utils.js index 656ce42654e7..249f7575cf63 100644 --- a/services/static-webserver/client/source/class/osparc/product/Utils.js +++ b/services/static-webserver/client/source/class/osparc/product/Utils.js @@ -189,7 +189,7 @@ qx.Class.define("osparc.product.Utils", { }, hasConvertToPipelineEnabled: function() { - return false; + return osparc.store.StaticInfo.getInstance().isDevFeaturesEnabled(); }, // oSPARC only diff --git a/services/static-webserver/client/source/class/osparc/study/StudyPreview.js b/services/static-webserver/client/source/class/osparc/study/StudyPreview.js index c1d6af633be7..7d2a101ba53f 100644 --- a/services/static-webserver/client/source/class/osparc/study/StudyPreview.js +++ b/services/static-webserver/client/source/class/osparc/study/StudyPreview.js @@ -37,7 +37,7 @@ qx.Class.define("osparc.study.StudyPreview", { __buildPreview: function() { const study = this.__study; const uiMode = study.getUi().getMode(); - if (uiMode === "workbench" && !study.isPipelineEmpty()) { + if (["workbench", "pipeline"].includes(uiMode) && !study.isPipelineEmpty()) { const workbenchUIPreview = new osparc.workbench.WorkbenchUIPreview(); workbenchUIPreview.setStudy(study); workbenchUIPreview.loadModel(study.getWorkbench()); diff --git a/services/static-webserver/client/source/class/osparc/study/Utils.js b/services/static-webserver/client/source/class/osparc/study/Utils.js index faf17ed124dd..8faf8bf1b516 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -23,75 +23,6 @@ qx.Class.define("osparc.study.Utils", { type: "static", statics: { - isAnyLinkedNodeMissing: function(studyData) { - const existingNodeIds = Object.keys(studyData["workbench"]); - const linkedNodeIds = osparc.data.model.Workbench.getLinkedNodeIds(studyData["workbench"]); - const allExist = linkedNodeIds.every(linkedNodeId => existingNodeIds.includes(linkedNodeId)); - return !allExist; - }, - - extractUniqueServices: function(workbench) { - const services = new Set([]); - Object.values(workbench).forEach(srv => { - services.add({ - key: srv.key, - version: srv.version - }); - }); - return Array.from(services); - }, - - getCantExecuteServices: function(studyServices = []) { - return studyServices.filter(studyService => studyService["myAccessRights"]["execute"] === false); - }, - - anyServiceRetired: function(studyServices) { - const isRetired = studyServices.some(studyService => { - if (studyService["release"] && studyService["release"]["retired"]) { - const retirementDate = new Date(studyService["release"]["retired"]); - const currentDate = new Date(); - return retirementDate < currentDate; - } - return false; - }); - return isRetired; - }, - - anyServiceDeprecated: function(studyServices) { - const isDeprecated = studyServices.some(studyService => { - if (studyService["release"] && studyService["release"]["retired"]) { - const retirementDate = new Date(studyService["release"]["retired"]); - const currentDate = new Date(); - return retirementDate > currentDate; - } - return false; - }); - return isDeprecated; - }, - - anyServiceUpdatable: function(studyServices) { - const isUpdatable = studyServices.some(studyService => { - if (studyService["release"] && studyService["release"]["compatibility"]) { - return Boolean(studyService["release"]["compatibility"]); - } - return false; - }); - return isUpdatable; - }, - - updatableNodeIds: function(workbench, studyServices) { - const nodeIds = []; - for (const nodeId in workbench) { - const node = workbench[nodeId]; - const studyServiceFound = studyServices.find(studyService => studyService["key"] === node["key"] && studyService["release"]["version"] === node["version"]); - if (studyServiceFound && studyServiceFound["release"] && studyServiceFound["release"]["compatibility"]) { - nodeIds.push(nodeId); - } - } - return nodeIds; - }, - - createStudyFromService: function(key, version, existingStudies, newStudyLabel, contextProps = {}) { return new Promise((resolve, reject) => { osparc.store.Services.getService(key, version) @@ -257,6 +188,91 @@ qx.Class.define("osparc.study.Utils", { }); }, + duplicateStudy: function(studyData) { + const text = qx.locale.Manager.tr("Duplicate process started and added to the background tasks"); + osparc.FlashMessenger.logAs(text, "INFO"); + + const params = { + url: { + "studyId": studyData["uuid"] + } + }; + const options = { + pollTask: true + }; + const fetchPromise = osparc.data.Resources.fetch("studies", "duplicate", params, options); + const pollTasks = osparc.store.PollTasks.getInstance(); + return pollTasks.createPollingTask(fetchPromise) + }, + + isAnyLinkedNodeMissing: function(studyData) { + const existingNodeIds = Object.keys(studyData["workbench"]); + const linkedNodeIds = osparc.data.model.Workbench.getLinkedNodeIds(studyData["workbench"]); + const allExist = linkedNodeIds.every(linkedNodeId => existingNodeIds.includes(linkedNodeId)); + return !allExist; + }, + + extractUniqueServices: function(workbench) { + const services = new Set([]); + Object.values(workbench).forEach(srv => { + services.add({ + key: srv.key, + version: srv.version + }); + }); + return Array.from(services); + }, + + getCantExecuteServices: function(studyServices = []) { + return studyServices.filter(studyService => studyService["myAccessRights"]["execute"] === false); + }, + + anyServiceRetired: function(studyServices) { + const isRetired = studyServices.some(studyService => { + if (studyService["release"] && studyService["release"]["retired"]) { + const retirementDate = new Date(studyService["release"]["retired"]); + const currentDate = new Date(); + return retirementDate < currentDate; + } + return false; + }); + return isRetired; + }, + + anyServiceDeprecated: function(studyServices) { + const isDeprecated = studyServices.some(studyService => { + if (studyService["release"] && studyService["release"]["retired"]) { + const retirementDate = new Date(studyService["release"]["retired"]); + const currentDate = new Date(); + return retirementDate > currentDate; + } + return false; + }); + return isDeprecated; + }, + + anyServiceUpdatable: function(studyServices) { + const isUpdatable = studyServices.some(studyService => { + if (studyService["release"] && studyService["release"]["compatibility"]) { + return Boolean(studyService["release"]["compatibility"]); + } + return false; + }); + return isUpdatable; + }, + + updatableNodeIds: function(workbench, studyServices) { + const nodeIds = []; + for (const nodeId in workbench) { + const node = workbench[nodeId]; + const studyServiceFound = studyServices.find(studyService => studyService["key"] === node["key"] && studyService["release"]["version"] === node["version"]); + if (studyServiceFound && studyServiceFound["release"] && studyServiceFound["release"]["compatibility"]) { + nodeIds.push(nodeId); + } + } + return nodeIds; + }, + isInDebt: function(studyData) { return Boolean("debt" in studyData && studyData["debt"] < 0); }, @@ -340,30 +356,37 @@ qx.Class.define("osparc.study.Utils", { }, guessIcon: function(studyData) { - if (osparc.product.Utils.isProduct("tis") || osparc.product.Utils.isProduct("tiplite")) { + if ( + (osparc.product.Utils.isProduct("tis") || osparc.product.Utils.isProduct("tiplite")) && + ["app", "guided"].includes(studyData["ui"]["mode"]) + ) { return new Promise(resolve => resolve(this.__guessTIPIcon(studyData))); } return this.__guessIcon(studyData); }, __guessIcon: function(studyData) { - const defaultIcon = osparc.dashboard.CardBase.PRODUCT_ICON; return new Promise(resolve => { - // the was to guess the TI type is to check the boot mode of the ti-postpro in the pipeline - const wbServices = this.self().getNonFrontendNodes(studyData); - if (wbServices.length === 1) { - const wbService = wbServices[0]; - osparc.store.Services.getService(wbService.key, wbService.version) - .then(serviceMetadata => { - if (serviceMetadata && serviceMetadata["icon"]) { - resolve(serviceMetadata["icon"]); - } - resolve(defaultIcon); - }); - } else if (wbServices.length > 1) { + if (studyData["ui"]["mode"] === "pipeline") { resolve("osparc/icons/diagram.png"); } else { - resolve(defaultIcon); + const defaultIcon = osparc.dashboard.CardBase.PRODUCT_ICON; + // the was to guess the TI type is to check the boot mode of the ti-postpro in the pipeline + const wbServices = this.self().getNonFrontendNodes(studyData); + if (wbServices.length === 1) { + const wbService = wbServices[0]; + osparc.store.Services.getService(wbService.key, wbService.version) + .then(serviceMetadata => { + if (serviceMetadata && serviceMetadata["icon"]) { + resolve(serviceMetadata["icon"]); + } + resolve(defaultIcon); + }); + } else if (wbServices.length > 1) { + resolve("osparc/icons/diagram.png"); + } else { + resolve(defaultIcon); + } } }); }, diff --git a/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js b/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js index 2f60ff3c5f36..aafb1aac589a 100644 --- a/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js +++ b/services/static-webserver/client/source/class/osparc/ui/window/Confirmation.js @@ -55,7 +55,6 @@ qx.Class.define("osparc.ui.window.Confirmation", { let control; switch (id) { case "confirm-button": { - const btnsLayout = this.getChildControl("buttons-layout"); control = new qx.ui.form.Button().set({ appearance: "form-button", center: true, @@ -67,6 +66,7 @@ qx.Class.define("osparc.ui.window.Confirmation", { }, this); const command = new qx.ui.command.Command("Enter"); control.setCommand(command); + const btnsLayout = this.getChildControl("buttons-layout"); btnsLayout.addAt(control, 1); break; }