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 e18ad26b552..6d74de34376 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -320,7 +320,7 @@ qx.Class.define("osparc.dashboard.CardBase", { trashedAt: { check: "Date", - apply: "_applyTrasehdAt", + apply: "_applyTrashedAt", nullable: true }, @@ -385,15 +385,15 @@ qx.Class.define("osparc.dashboard.CardBase", { apply: "__applyState" }, - projectState: { - check: ["NOT_STARTED", "STARTED", "SUCCESS", "FAILED", "UNKNOWN"], - nullable: false, - init: "UNKNOWN", - apply: "_applyProjectState" + debt: { + check: "Number", + nullable: true, + init: 0, + apply: "__applyDebt" }, blocked: { - check: [true, "UNKNOWN_SERVICES", "IN_USE", false], + check: [true, "UNKNOWN_SERVICES", "IN_USE", "IN_DEBT", false], init: false, nullable: false, apply: "__applyBlocked" @@ -547,7 +547,7 @@ qx.Class.define("osparc.dashboard.CardBase", { throw new Error("Abstract method called!"); }, - _applyTrasehdAt: function(value, old) { + _applyTrashedAt: function(value, old) { throw new Error("Abstract method called!"); }, @@ -633,7 +633,7 @@ qx.Class.define("osparc.dashboard.CardBase", { const unaccessibleServices = osparc.study.Utils.getInaccessibleServices(workbench) if (unaccessibleServices.length) { this.setBlocked("UNKNOWN_SERVICES"); - const image = "@FontAwesome5Solid/ban/"; + let image = "@FontAwesome5Solid/ban/"; let toolTipText = this.tr("Service info missing"); unaccessibleServices.forEach(unSrv => { toolTipText += "
" + unSrv.key + ":" + unSrv.version; @@ -681,65 +681,75 @@ qx.Class.define("osparc.dashboard.CardBase", { }, __applyState: function(state) { - const locked = ("locked" in state) ? state["locked"]["value"] : false; - this.setBlocked(locked ? "IN_USE" : false); - if (locked) { - this.__showBlockedCardFromStatus(state["locked"]); + let lockInUse = false; + if ("locked" in state && "value" in state["locked"]) { + lockInUse = state["locked"]["value"]; + } + this.setBlocked(lockInUse ? "IN_USE" : false); + if (lockInUse) { + this.__showBlockedCardFromStatus("IN_USE", state["locked"]); } - const projectState = ("state" in state) ? state["state"]["value"] : undefined; - if (projectState) { - this._applyProjectState(state["state"]); + const pipelineState = ("state" in state) ? state["state"]["value"] : undefined; + if (pipelineState) { + this.__applyPipelineState(state["state"]["value"]); } }, - _applyProjectState: function(projectStatus) { - const status = projectStatus["value"]; - let icon; - let toolTip; - let border; - switch (status) { + __applyDebt: function(debt) { + this.setBlocked(debt ? "IN_DEBT" : false); + if (debt) { + this.__showBlockedCardFromStatus("IN_DEBT", debt); + } + }, + + // pipelineState: ["NOT_STARTED", "STARTED", "SUCCESS", "ABORTED", "FAILED", "UNKNOWN"] + __applyPipelineState: function(pipelineState) { + let iconSource; + let toolTipText; + let borderColor; + switch (pipelineState) { case "STARTED": - icon = "@FontAwesome5Solid/spinner/10"; - toolTip = this.tr("Running"); - border = "info"; + iconSource = "@FontAwesome5Solid/spinner/10"; + toolTipText = this.tr("Running"); + borderColor = "info"; break; case "SUCCESS": - icon = "@FontAwesome5Solid/check/10"; - toolTip = this.tr("Ran successfully"); - border = "success"; + iconSource = "@FontAwesome5Solid/check/10"; + toolTipText = this.tr("Ran successfully"); + borderColor = "success"; break; case "ABORTED": - icon = "@FontAwesome5Solid/exclamation/10"; - toolTip = this.tr("Run aborted"); - border = "warning"; + iconSource = "@FontAwesome5Solid/exclamation/10"; + toolTipText = this.tr("Run aborted"); + borderColor = "warning"; break; case "FAILED": - icon = "@FontAwesome5Solid/exclamation/10"; - toolTip = this.tr("Ran with error"); - border = "error"; + iconSource = "@FontAwesome5Solid/exclamation/10"; + toolTipText = this.tr("Ran with error"); + borderColor = "error"; break; + case "UNKNOWN": + case "NOT_STARTED": default: - icon = null; - toolTip = null; - border = null; + iconSource = null; + toolTipText = null; + borderColor = null; break; } - this.__applyProjectLabel(icon, toolTip, border); - }, - __applyProjectLabel: function(icn, toolTipText, bdr) { const border = new qx.ui.decoration.Decorator().set({ radius: 10, width: 1, style: "solid", - color: bdr, - backgroundColor: bdr ? bdr + "-bg" : null + color: borderColor, + backgroundColor: borderColor ? borderColor + "-bg" : null }); + const projectStatusLabel = this.getChildControl("project-status"); projectStatusLabel.set({ decorator: border, - textColor: bdr, + textColor: borderColor, alignX: "center", alignY: "middle", height: 17, @@ -748,14 +758,25 @@ qx.Class.define("osparc.dashboard.CardBase", { }); projectStatusLabel.set({ - visibility: icn && toolTipText && bdr ? "visible" : "excluded", - source: icn, - toolTipIcon: icn, + visibility: iconSource && toolTipText && borderColor ? "visible" : "excluded", + source: iconSource, + toolTipIcon: iconSource, toolTipText }); }, - __showBlockedCardFromStatus: function(lockedStatus) { + __showBlockedCardFromStatus: function(reason, moreInfo) { + switch (reason) { + case "IN_USE": + this.__blockedInUse(moreInfo); + break; + case "IN_DEBT": + this.__blockedInDebt(moreInfo); + break; + } + }, + + __blockedInUse: function(lockedStatus) { const status = lockedStatus["status"]; const owner = lockedStatus["owner"]; let toolTip = osparc.utils.Utils.firstsUp(owner["first_name"] || this.tr("A user"), owner["last_name"] || ""); // it will be replaced by "userName" @@ -788,14 +809,23 @@ qx.Class.define("osparc.dashboard.CardBase", { this.__showBlockedCard(image, toolTip); }, + __blockedInDebt: function() { + const studyAlias = osparc.product.Utils.getStudyAlias({firstUpperCase: true}); + const toolTip = studyAlias + " " + this.tr("Embargoed
Credits Required"); + const image = "@FontAwesome5Solid/lock/"; + this.__showBlockedCard(image, toolTip); + }, + __showBlockedCard: function(lockImageSrc, toolTipText) { this.getChildControl("lock-status").set({ opacity: 1.0, visibility: "visible" }); + const lockImage = this.getChildControl("lock-status").getChildControl("image"); lockImageSrc += this.classname.includes("Grid") ? "32" : "22"; lockImage.setSource(lockImageSrc); + if (toolTipText) { this.set({ toolTipText diff --git a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js index 13879b4f0a4..22779e7831b 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonItem.js @@ -188,7 +188,7 @@ qx.Class.define("osparc.dashboard.GridButtonItem", { }, // overridden - _applyTrasehdAt: function(value) { + _applyTrashedAt: function(value) { if (value && value.getTime() !== new Date(0).getTime()) { if (this.isResourceType("study") || this.isResourceType("template")) { const dateBy = this.getChildControl("date-by"); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js index 049e22f9930..e81d4369ba5 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ListButtonItem.js @@ -204,7 +204,7 @@ qx.Class.define("osparc.dashboard.ListButtonItem", { }, // overridden - _applyTrasehdAt: function(value) { + _applyTrashedAt: function(value) { if (value && value.getTime() !== new Date(0).getTime()) { if (this.isResourceType("study") || this.isResourceType("template")) { const dateBy = this.getChildControl("date-by"); 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 83587f0dbe0..5cd54f4b231 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -67,6 +67,16 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { height: this.HEIGHT, }); return win; + }, + + createToolbar: function() { + const toolbar = new qx.ui.container.Composite(new qx.ui.layout.HBox(20).set({ + alignX: "right", + alignY: "top" + })).set({ + maxHeight: 40 + }); + return toolbar; } }, @@ -90,32 +100,36 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { __billingSettings: null, __classifiersPage: null, __qualityPage: null, - __openButton: null, - - __createToolbar: function() { - const toolbar = new qx.ui.container.Composite(new qx.ui.layout.HBox(20).set({ - alignX: "right", - alignY: "top" - })).set({ - maxHeight: 40 - }); - return toolbar; - }, __addOpenButton: function(page) { const resourceData = this.__resourceData; - const toolbar = this.__createToolbar(); + const toolbar = this.self().createToolbar(); page.addToHeader(toolbar); + if (this.__resourceData["resourceType"] === "study") { + const payDebtButton = new qx.ui.form.Button(this.tr("Credits required")); + page.payDebtButton = payDebtButton; + osparc.dashboard.resources.pages.BasePage.decorateHeaderButton(payDebtButton); + payDebtButton.addListener("execute", () => this.openBillingSettings()); + if (this.__resourceData["resourceType"] === "study") { + const studyData = this.__resourceData; + payDebtButton.set({ + visibility: osparc.study.Utils.isInDebt(studyData) ? "visible" : "excluded" + }); + } + toolbar.add(payDebtButton); + } + if (osparc.utils.Resources.isService(resourceData)) { const serviceVersionSelector = this.__createServiceVersionSelector(); toolbar.add(serviceVersionSelector); } - const openButton = this.__openButton = new osparc.ui.form.FetchButton(this.tr("Open")).set({ + const openButton = new osparc.ui.form.FetchButton(this.tr("Open")).set({ enabled: true }); + page.openButton = openButton; osparc.dashboard.resources.pages.BasePage.decorateHeaderButton(openButton); osparc.utils.Utils.setIdToWidget(openButton, "openResource"); const store = osparc.store.Store.getInstance(); @@ -125,8 +139,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.bind("showOpenButton", openButton, "visibility", { converter: show => (store.getCurrentStudy() === null && show) ? "visible" : "excluded" }); - - openButton.addListener("execute", () => this.__openTapped()); + openButton.addListener("execute", () => this.__openTapped(openButton)); if (this.__resourceData["resourceType"] === "study") { const studyData = this.__resourceData; @@ -137,13 +150,13 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { toolbar.add(openButton); }, - __openTapped: function() { + __openTapped: function(openButton) { if (this.__resourceData["resourceType"] !== "study") { // Template or Service, nothing to pre-check this.__openResource(); return; } - this.__openButton.setFetching(true); + openButton.setFetching(true); const params = { url: { "studyId": this.__resourceData["uuid"] @@ -151,7 +164,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { }; osparc.data.Resources.getOne("studies", params) .then(updatedStudyData => { - this.__openButton.setFetching(false); + openButton.setFetching(false); const updatableServices = osparc.metadata.ServicesInStudyUpdate.updatableNodeIds(updatedStudyData.workbench); if (updatableServices.length && osparc.data.model.Study.canIWrite(updatedStudyData["accessRights"])) { this.__confirmUpdate(); @@ -162,7 +175,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { .catch(err => { console.error(err); osparc.FlashMessenger.logAs(err.message, "ERROR"); - this.__openButton.setFetching(false); + openButton.setFetching(false); }); }, @@ -365,19 +378,27 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const resourceData = this.__resourceData; if (osparc.utils.Resources.isStudy(resourceData)) { const id = "Billing"; - const title = this.tr("Tier Settings"); + const title = this.tr("Billing Settings"); const iconSrc = "@FontAwesome5Solid/cogs/22"; const page = this.__billingSettings = new osparc.dashboard.resources.pages.BasePage(title, iconSrc, id); this.__addOpenButton(page); - if (this.__resourceData["resourceType"] === "study") { - const studyData = this.__resourceData; - const canBeOpened = osparc.study.Utils.canShowBillingOptions(studyData); + if (resourceData["resourceType"] === "study") { + const canBeOpened = osparc.study.Utils.canShowBillingOptions(resourceData); page.setEnabled(canBeOpened); } const lazyLoadContent = () => { const billingSettings = new osparc.study.BillingSettings(resourceData); + billingSettings.addListener("debtPayed", () => { + if (resourceData["resourceType"] === "study") { + page.payDebtButton.set({ + visibility: osparc.study.Utils.isInDebt(resourceData) ? "visible" : "excluded" + }); + const canBeOpened = osparc.study.Utils.canBeOpened(resourceData); + page.openButton.setEnabled(canBeOpened); + } + }) const billingScroll = new qx.ui.container.Scroll(billingSettings); page.addToContent(billingScroll); } @@ -751,7 +772,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const publishTemplateButton = saveAsTemplate.getPublishTemplateButton(); osparc.dashboard.resources.pages.BasePage.decorateHeaderButton(publishTemplateButton); - const toolbar = this.__createToolbar(); + const toolbar = this.self().createToolbar(); toolbar.add(publishTemplateButton); page.addToHeader(toolbar); page.addToContent(saveAsTemplate); 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 a3dfabaf46e..1b4c523639e 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js @@ -732,10 +732,26 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { this.resetSelection(); this.setMultiSelection(false); }); - osparc.store.Store.getInstance().addListener("changeTags", () => { + + const store = osparc.store.Store.getInstance(); + store.addListener("changeTags", () => { this.invalidateStudies(); this.__reloadStudies(); }, this); + store.addListener("studyStateChanged", e => { + const { + studyId, + state, + } = e.getData(); + this.__studyStateChanged(studyId, state); + }); + store.addListener("studyDebtChanged", e => { + const { + studyId, + debt, + } = e.getData(); + this.__studyDebtChanged(studyId, debt); + }); qx.event.message.Bus.subscribe("reloadStudies", () => { this.invalidateStudies(); @@ -1065,7 +1081,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { studiesDeleteButton.set({ visibility: selection.length && currentContext === "trash" ? "visible" : "excluded", - label: this.tr("Delete permamently") + (selection.length > 1 ? this.tr(" selected ") + `(${selection.length})` : ""), + label: this.tr("Delete permanently") + (selection.length > 1 ? this.tr(" selected ") + `(${selection.length})` : ""), }); }); @@ -1420,6 +1436,12 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { __studyStateReceived: function(studyId, state, errors) { osparc.store.Store.getInstance().setStudyState(studyId, state); + if (errors && errors.length) { + console.error(errors); + } + }, + + __studyStateChanged: function(studyId, state) { const idx = this._resourcesList.findIndex(study => study["uuid"] === studyId); if (idx > -1) { this._resourcesList[idx]["state"] = state; @@ -1428,8 +1450,16 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { if (studyItem) { studyItem.setState(state); } - if (errors && errors.length) { - console.error(errors); + }, + + __studyDebtChanged: function(studyId, debt) { + const idx = this._resourcesList.findIndex(study => study["uuid"] === studyId); + if (idx > -1) { + this._resourcesList[idx]["debt"] = debt; + } + const studyItem = this._resourcesContainer.getCards().find(card => osparc.dashboard.ResourceBrowserBase.isCardButtonItem(card) && card.getUuid() === studyId); + if (studyItem) { + studyItem.setDebt(debt); } }, @@ -1694,7 +1724,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }, __getBillingMenuButton: function(card) { - const text = osparc.utils.Utils.capitalize(this.tr("Tier Settings...")); + const text = osparc.utils.Utils.capitalize(this.tr("Billing Settings...")); const studyBillingSettingsButton = new qx.ui.menu.Button(text); studyBillingSettingsButton["billingSettingsButton"] = true; studyBillingSettingsButton.addListener("tap", () => card.openBilling(), this); diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 00808b4b299..40786ae0050 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -162,6 +162,10 @@ qx.Class.define("osparc.data.Resources", { method: "PUT", url: statics.API + "/projects/{studyId}/wallet/{walletId}" }, + payDebt: { + method: "POST", + url: statics.API + "/projects/{studyId}/wallet/{walletId}:pay-debt" + }, openDisableAutoStart: { method: "POST", url: statics.API + "/projects/{studyId}:open?disable_service_auto_start={disableServiceAutoStart}" diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js index 0ccb9bbe8b9..39968f8913d 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js @@ -89,11 +89,7 @@ qx.Class.define("osparc.desktop.MainPage", { const socket = osparc.wrapper.WebSocket.getInstance(); if (!socket.slotExists("walletOsparcCreditsUpdated")) { socket.on("walletOsparcCreditsUpdated", data => { - const store = osparc.store.Store.getInstance(); - const walletFound = store.getWallets().find(wallet => wallet.getWalletId() === parseInt(data["wallet_id"])); - if (walletFound) { - walletFound.setCreditsAvailable(parseFloat(data["osparc_credits"])); - } + osparc.desktop.credits.Utils.creditsUpdated(data["wallet_id"], data["osparc_credits"]); }, this); } }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js b/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js index 40c99616a40..ee935adab67 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPageDesktop.js @@ -78,11 +78,7 @@ qx.Class.define("osparc.desktop.MainPageDesktop", { const socket = osparc.wrapper.WebSocket.getInstance(); if (!socket.slotExists("walletOsparcCreditsUpdated")) { socket.on("walletOsparcCreditsUpdated", data => { - const store = osparc.store.Store.getInstance(); - const walletFound = store.getWallets().find(wallet => wallet.getWalletId() === parseInt(data["wallet_id"])); - if (walletFound) { - walletFound.setCreditsAvailable(parseFloat(data["osparc_credits"])); - } + osparc.desktop.credits.Utils.creditsUpdated(data["wallet_id"], data["osparc_credits"]); }, this); } } 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 bfd02dc41eb..62a759623f6 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -183,11 +183,30 @@ qx.Class.define("osparc.desktop.StudyEditor", { .catch(err => { console.error(err); let msg = ""; - if ("status" in err && err["status"] == 409) { // max_open_studies_per_user - msg = err["message"]; - } else if ("status" in err && err["status"] == 423) { // Locked - msg = study.getName() + this.tr(" is already opened"); - } else { + if ("status" in err && err["status"]) { + if (err["status"] == 402) { + msg = err["message"]; + // The backend might have thrown a 402 because the wallet was negative + const match = msg.match(/Project debt\s([-]?\d+(\.\d+)?)\scredits/); + let debt = null; + if ("debtAmount" in err) { + // the study has some debt that needs to be paid + debt = err["debtAmount"]; + } else if (match) { + // the study has some debt that needs to be paid + debt = parseFloat(match[1]); // Convert the captured string to a number + } + if (debt) { + // if get here, it means that the 402 was thrown due to the debt + osparc.store.Store.getInstance().setStudyDebt(study.getUuid(), debt); + } + } else if (err["status"] == 409) { // max_open_studies_per_user + msg = err["message"]; + } else if (err["status"] == 423) { // Locked + msg = study.getName() + this.tr(" is already opened"); + } + } + if (!msg) { msg = this.tr("Error opening study"); if ("message" in err) { msg += "
" + err["message"]; diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js b/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js index 8e68c41070c..d27acc99fe0 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/BillingCenter.js @@ -126,18 +126,7 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", { __openBuyCredits: function() { if (this.__paymentMethods) { const paymentMethods = this.__paymentMethods.getPaymentMethods(); - const buyView = new osparc.desktop.credits.BuyCreditsStepper( - paymentMethods.map(({idr, cardHolderName, cardNumberMasked}) => ({ - label: `${cardHolderName} ${cardNumberMasked}`, - id: idr - })) - ); - const win = osparc.ui.window.Window.popUpInWindow(buyView, "Buy credits", 400, 600).set({ - resizable: false, - movable: false - }); - buyView.addListener("completed", () => win.close()); - win.addListener("close", () => buyView.cancelPayment()) + osparc.desktop.credits.Utils.openBuyCredits(paymentMethods); } }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCreditsStepper.js b/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCreditsStepper.js index ca6118ecb6e..f569fb05b34 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCreditsStepper.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCreditsStepper.js @@ -17,7 +17,8 @@ qx.Class.define("osparc.desktop.credits.BuyCreditsStepper", { this.__buildLayout() }, events: { - "completed": "qx.event.type.Event" + "completed": "qx.event.type.Event", + "cancelled": "qx.event.type.Event", }, properties: { paymentId: { @@ -85,7 +86,7 @@ qx.Class.define("osparc.desktop.credits.BuyCreditsStepper", { .finally(() => this.__form.setFetching(false)); } }); - this.__form.addListener("cancel", () => this.fireEvent("completed")); + this.__form.addListener("cancel", () => this.fireEvent("cancelled")); this.add(this.__form); this.setSelection([this.__form]) }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js index e05d53427da..a797f06a79a 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js @@ -22,6 +22,35 @@ qx.Class.define("osparc.desktop.credits.Utils", { DANGER_ZONE: 25, // one hour consumption CREDITS_ICON: "@FontAwesome5Solid/database/", + creditsUpdated: function(walletId, credits) { + const store = osparc.store.Store.getInstance(); + const walletFound = store.getWallets().find(wallet => wallet.getWalletId() === parseInt(walletId)); + if (walletFound) { + walletFound.setCreditsAvailable(parseFloat(credits)); + } + }, + + openBuyCredits: function(paymentMethods = []) { + const buyView = new osparc.desktop.credits.BuyCreditsStepper( + paymentMethods.map(({idr, cardHolderName, cardNumberMasked}) => ({ + label: `${cardHolderName} ${cardNumberMasked}`, + id: idr + })) + ); + const win = osparc.ui.window.Window.popUpInWindow(buyView, "Buy credits", 400, 600).set({ + resizable: false, + movable: false + }); + buyView.addListener("completed", () => win.close()); + buyView.addListener("cancelled", () => win.close()); + win.addListener("close", () => buyView.cancelPayment()) + return { + window: win, + buyCreditsWidget: buyView, + }; + }, + + areWalletsEnabled: function() { const statics = osparc.store.Store.getInstance().get("statics"); return Boolean(statics && statics["isPaymentEnabled"]); @@ -74,7 +103,7 @@ qx.Class.define("osparc.desktop.credits.Utils", { const store = osparc.store.Store.getInstance(); const walletSelector = new qx.ui.form.SelectBox().set({ - minWidth: 220 + maxWidth: 250 }); const populateSelectBox = selectBox => { diff --git a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletListItem.js b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletListItem.js index e184975f3a3..2c2a70a7f07 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletListItem.js +++ b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletListItem.js @@ -247,24 +247,24 @@ qx.Class.define("osparc.desktop.wallets.WalletListItem", { _applyAccessRights: function(accessRights) { this.__buildLayout(); this.base(arguments, accessRights); - this.__buyBtn = new qx.ui.form.Button().set({ + const buyBtn = new qx.ui.form.Button().set({ label: this.tr("Buy Credits"), icon: "@FontAwesome5Solid/dollar-sign/16", maxHeight: 30, alignY: "middle", visibility: this.__canIWrite() ? "visible" : "excluded", }); - osparc.utils.Utils.setIdToWidget(this.__buyBtn, "buyCreditsBtn"); - this.bind("accessRights", this.__buyBtn, "enabled", { + osparc.utils.Utils.setIdToWidget(buyBtn, "buyCreditsBtn"); + this.bind("accessRights", buyBtn, "enabled", { converter: aR => { const myAr = osparc.data.model.Wallet.getMyAccessRights(aR); return Boolean(myAr && myAr.write); } }); - this.__buyBtn.addListener("execute", () => this.fireDataEvent("buyCredits", { + buyBtn.addListener("execute", () => this.fireDataEvent("buyCredits", { walletId: this.getKey() }), this); - this._add(this.__buyBtn, { + this._add(buyBtn, { row: 0, column: 6, rowSpan: 2 diff --git a/services/static-webserver/client/source/class/osparc/store/Store.js b/services/static-webserver/client/source/class/osparc/store/Store.js index e9146a1402f..81a1c4e9dcd 100644 --- a/services/static-webserver/client/source/class/osparc/store/Store.js +++ b/services/static-webserver/client/source/class/osparc/store/Store.js @@ -228,6 +228,11 @@ qx.Class.define("osparc.store.Store", { }, }, + events: { + "studyStateChanged": "qx.event.type.Data", + "studyDebtChanged": "qx.event.type.Data", + }, + members: { // fetch resources that do not require log in preloadCalls: async function() { @@ -422,6 +427,28 @@ qx.Class.define("osparc.store.Store", { if (currentStudy && currentStudy.getUuid() === studyId) { currentStudy.setState(state); } + + this.fireDataEvent("studyStateChanged", { + studyId, + state, + }); + }, + + setStudyDebt: function(studyId, debt) { + const studiesWStateCache = this.getStudies(); + const idx = studiesWStateCache.findIndex(studyWStateCache => studyWStateCache["uuid"] === studyId); + if (idx !== -1) { + if (debt) { + studiesWStateCache[idx]["debt"] = debt; + } else { + delete studiesWStateCache[idx]["debt"]; + } + } + + this.fireDataEvent("studyDebtChanged", { + studyId, + debt, + }); }, setTemplateState: function(templateId, state) { diff --git a/services/static-webserver/client/source/class/osparc/study/BillingSettings.js b/services/static-webserver/client/source/class/osparc/study/BillingSettings.js index d74093a326f..623fc9ae0f3 100644 --- a/services/static-webserver/client/source/class/osparc/study/BillingSettings.js +++ b/services/static-webserver/client/source/class/osparc/study/BillingSettings.js @@ -24,92 +24,278 @@ qx.Class.define("osparc.study.BillingSettings", { construct: function(studyData) { this.base(arguments); - this._setLayout(new qx.ui.layout.VBox(5)); + this._setLayout(new qx.ui.layout.VBox(10)); this.__studyData = studyData; this.__buildLayout(); }, + events: { + "debtPayed": "qx.event.type.Event", + }, + members: { __studyData: null, + __studyWalletId: null, + __debtMessage: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "credit-account-box": + control = osparc.study.StudyOptions.createGroupBox(this.tr("Credit Account")); + this._add(control); + break; + case "wallet-selector": + control = osparc.desktop.credits.Utils.createWalletSelector("read"); + this.getChildControl("credit-account-box").add(control); + break; + case "pay-debt-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5).set({ + alignY: "middle", + })); + this.getChildControl("credit-account-box").add(control); + break; + case "debt-explanation": + control = new qx.ui.basic.Label().set({ + rich: true, + wrap: true, + }); + this.getChildControl("pay-debt-layout").add(control); + break; + case "buy-credits-button": + control = new qx.ui.form.Button().set({ + label: this.tr("Buy Credits"), + icon: "@FontAwesome5Solid/dollar-sign/14", + allowGrowX: false + }); + this.getChildControl("pay-debt-layout").add(control); + break; + case "transfer-debt-button": + control = new qx.ui.form.Button().set({ + label: this.tr("Transfer from this Credit Account"), + icon: "@FontAwesome5Solid/exchange-alt/14", + allowGrowX: false + }); + this.getChildControl("pay-debt-layout").add(control); + break; + } + return control || this.base(arguments, id); + }, __buildLayout: function() { + if (osparc.study.Utils.isInDebt(this.__studyData)) { + this.__buildDebtMessage(); + } this.__buildWalletGroup(); this.__buildPricingUnitsGroup(); }, - __buildWalletGroup: function() { - const pricingUnitsLayout = osparc.study.StudyOptions.createGroupBox(this.tr("Credit Account")); - - const populateCreditAccountBox = () => { - pricingUnitsLayout.removeAll(); - - const hBox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)).set({ - alignY: "middle" - }); + __buildDebtMessage: function() { + const border = new qx.ui.decoration.Decorator().set({ + radius: 4, + width: 1, + style: "solid", + color: "danger-red", + }); + const studyAlias = osparc.product.Utils.getStudyAlias(); + let msg = this.tr(`This ${studyAlias} is currently Embargoed.
`); + msg += this.tr("Last charge:") + "
"; + msg += this.__studyData["debt"] + " " + this.tr("credits"); + const debtMessage = this.__debtMessage = new qx.ui.basic.Label(msg).set({ + decorator: border, + font: "text-14", + rich: true, + padding: 10, + marginBottom: 5, + }); + this._add(debtMessage); + }, - const walletSelector = osparc.desktop.credits.Utils.createWalletSelector("read"); - hBox.add(walletSelector); + __buildWalletGroup: function() { + const boxContent = this.getChildControl("credit-account-box"); - pricingUnitsLayout.add(hBox); + const walletSelector = this.getChildControl("wallet-selector"); - const paramsGet = { - url: { - studyId: this.__studyData["uuid"] + const paramsGet = { + url: { + studyId: this.__studyData["uuid"] + } + }; + osparc.data.Resources.fetch("studies", "getWallet", paramsGet) + .then(wallet => { + if (wallet) { + this.__studyWalletId = wallet["walletId"]; + const walletFound = walletSelector.getSelectables().find(selectables => selectables.walletId === wallet["walletId"]); + if (walletFound) { + walletSelector.setSelection([walletFound]); + if (osparc.study.Utils.isInDebt(this.__studyData)) { + this.__addDebtLayout(wallet["walletId"]); + } + } else { + const emptyItem = new qx.ui.form.ListItem(""); + emptyItem.walletId = null; + walletSelector.add(emptyItem); + walletSelector.setSelection([emptyItem]); + const label = new qx.ui.basic.Label(this.tr("You don't have access to the last used Credit Account")); + boxContent.add(label); + } } - }; - osparc.data.Resources.fetch("studies", "getWallet", paramsGet) - .then(wallet => { + }) + .finally(() => { + walletSelector.addListener("changeSelection", () => { + const wallet = this.__getSelectedWallet(); if (wallet) { - const walletFound = walletSelector.getSelectables().find(selectables => selectables.walletId === wallet["walletId"]); - if (walletFound) { - walletSelector.setSelection([walletFound]); + const walletId = wallet.getWalletId(); + if (osparc.study.Utils.isInDebt(this.__studyData)) { + this.__addDebtLayout(walletId); } else { - const emptyItem = new qx.ui.form.ListItem(""); - emptyItem.walletId = null; - walletSelector.add(emptyItem); - walletSelector.setSelection([emptyItem]); - const label = new qx.ui.basic.Label(this.tr("You don't have access to the last used Credit Account")); - hBox.add(label); + this.__switchWallet(walletId); } } - }) - .finally(() => { - walletSelector.addListener("changeSelection", e => { - const selection = e.getData(); - if (selection.length) { - const walletId = selection[0].walletId; - if (walletId === null) { - return; - } - hBox.setEnabled(false); - const paramsPut = { - url: { - studyId: this.__studyData["uuid"], - walletId - } - }; - osparc.data.Resources.fetch("studies", "selectWallet", paramsPut) - .then(() => { - const msg = this.tr("Credit Account saved"); - osparc.FlashMessenger.getInstance().logAs(msg, "INFO"); - }) - .catch(err => { - console.error(err); - osparc.FlashMessenger.logAs(err.message, "ERROR"); - }) - .finally(() => { - hBox.setEnabled(true); - populateCreditAccountBox(); - }); - } - }); }); + }); + }, + + __getSelectedWallet: function() { + const walletSelector = this.getChildControl("wallet-selector"); + const selection = walletSelector.getSelection(); + if (selection.length) { + const walletId = selection[0].walletId; + if (walletId) { + const wallet = osparc.desktop.credits.Utils.getWallet(walletId); + if (wallet) { + return wallet; + } + } + } + return null; + }, + + __getStudyWallet: function() { + if (this.__studyWalletId) { + const wallet = osparc.desktop.credits.Utils.getWallet(this.__studyWalletId); + if (wallet) { + return wallet; + } + } + return null; + }, + + __addDebtLayout: function(walletId) { + const payDebtLayout = this.getChildControl("pay-debt-layout"); + payDebtLayout.removeAll(); + + const wallet = osparc.desktop.credits.Utils.getWallet(walletId); + const myWallets = osparc.desktop.credits.Utils.getMyWallets(); + if (myWallets.find(wllt => wllt === wallet)) { + // It's my wallet + this._createChildControlImpl("debt-explanation").set({ + value: this.tr("Top up the Credit Account:
Purchase additional credits to bring the Credit Account balance back to a positive value.") + }); + const buyCreditsButton = this._createChildControlImpl("buy-credits-button"); + buyCreditsButton.addListener("execute", () => this.__openBuyCreditsWindow(), this); + } else { + // It's a shared wallet + this._createChildControlImpl("debt-explanation").set({ + value: this.tr("Transfer credits from another Account:
Use this Credit Account to cover the negative balance.") + }); + const transferDebtButton = this._createChildControlImpl("transfer-debt-button"); + transferDebtButton.addListener("execute", () => this.__transferCredits(), this); + } + }, + + __openBuyCreditsWindow: function() { + const wallet = this.__getSelectedWallet(); + if (wallet) { + osparc.desktop.credits.Utils.getPaymentMethods(wallet.getWalletId()) + .then(paymentMethods => { + const { + buyCreditsWidget + } = osparc.desktop.credits.Utils.openBuyCredits(paymentMethods); + buyCreditsWidget.addListener("completed", () => { + // at this point we can assume that the study got unblocked + this.__debtPayed(); + }) + }); + } + }, + + __transferCredits: function() { + const originWallet = this.__getSelectedWallet(); + const destWallet = this.__getStudyWallet(); + let msg = this.tr("A credits transfer will be initiated to cover the negative balance:"); + msg += "
- " + this.tr("Credits to transfer: ") + -1*this.__studyData["debt"]; + msg += "
- " + this.tr("From: ") + originWallet.getName(); + msg += "
- " + this.tr("To: ") + destWallet.getName(); + const confirmationWin = new osparc.ui.window.Confirmation(msg).set({ + confirmText: this.tr("Transfer"), + }); + confirmationWin.open(); + confirmationWin.addListener("close", () => { + if (confirmationWin.getConfirmed()) { + this.__doTransferCredits(); + } + }, this); + }, + + __doTransferCredits: function() { + const wallet = this.__getSelectedWallet(); + const params = { + url: { + studyId: this.__studyData["uuid"], + walletId: wallet.getWalletId(), + }, + data: { + amount: this.__studyData["debt"], + } }; - populateCreditAccountBox(); + osparc.data.Resources.fetch("studies", "payDebt", params) + .then(() => { + // at this point we can assume that the study got unblocked + this.__debtPayed(); + // also switch the study's wallet to this one + this.__switchWallet(wallet.getWalletId()); + }) + .catch(err => { + console.error(err); + osparc.FlashMessenger.logAs(err.message, "ERROR"); + }); + }, - this._add(pricingUnitsLayout); + __debtPayed: function() { + delete this.__studyData["debt"]; + osparc.store.Store.getInstance().setStudyDebt(this.__studyData["uuid"], 0); + this.fireEvent("debtPayed"); + if (this.__debtMessage) { + this._remove(this.__debtMessage); + } + this.getChildControl("pay-debt-layout").removeAll(); + }, + + __switchWallet: function(walletId) { + const creditAccountBox = this.getChildControl("credit-account-box"); + creditAccountBox.setEnabled(false); + const paramsPut = { + url: { + studyId: this.__studyData["uuid"], + walletId + } + }; + osparc.data.Resources.fetch("studies", "selectWallet", paramsPut) + .then(() => { + this.__studyWalletId = walletId; + const msg = this.tr("Credit Account saved"); + osparc.FlashMessenger.getInstance().logAs(msg, "INFO"); + }) + .catch(err => { + console.error(err); + osparc.FlashMessenger.logAs(err.message, "ERROR"); + }) + .finally(() => { + creditAccountBox.setEnabled(true); + }); }, __buildPricingUnitsGroup: function() { 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 66ed40201f4..445b2f82c31 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -266,6 +266,10 @@ qx.Class.define("osparc.study.Utils", { }); }, + isInDebt: function(studyData) { + return Boolean("debt" in studyData && studyData["debt"] < 0); + }, + __getBlockedState: function(studyData) { if (studyData["workbench"]) { const unaccessibleServices = osparc.study.Utils.getInaccessibleServices(studyData["workbench"]) @@ -276,6 +280,9 @@ qx.Class.define("osparc.study.Utils", { if (studyData["state"] && studyData["state"]["locked"] && studyData["state"]["locked"]["value"]) { return "IN_USE"; } + if (this.isInDebt(studyData)) { + return "IN_DEBT"; + } return false; }, @@ -286,7 +293,7 @@ qx.Class.define("osparc.study.Utils", { canShowBillingOptions: function(studyData) { const blocked = this.__getBlockedState(studyData); - return [false].includes(blocked); + return ["IN_DEBT", false].includes(blocked); }, canShowServiceUpdates: function(studyData) { diff --git a/services/static-webserver/client/source/class/osparc/ui/basic/DateAndBy.js b/services/static-webserver/client/source/class/osparc/ui/basic/DateAndBy.js index abc819dc8d9..320a5562f71 100644 --- a/services/static-webserver/client/source/class/osparc/ui/basic/DateAndBy.js +++ b/services/static-webserver/client/source/class/osparc/ui/basic/DateAndBy.js @@ -80,6 +80,7 @@ qx.Class.define("osparc.ui.basic.DateAndBy", { const label = this.getChildControl("date-text"); const today = new Date(); const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); if (today.toDateString() === value.toDateString()) { label.setValue(osparc.utils.Utils.formatDateAndTime(value)); // show date and time } else if (yesterday.toDateString() === value.toDateString()) {