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()) {