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 b1c1d81ffa6..534e9bd723a 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -631,7 +631,7 @@ qx.Class.define("osparc.data.Resources", { * PRICING PLANS */ "pricingPlans": { - useCache: true, + useCache: false, // handled in osparc.store.Pricing endpoints: { get: { method: "GET", @@ -656,7 +656,7 @@ qx.Class.define("osparc.data.Resources", { * PRICING UNITS */ "pricingUnits": { - useCache: true, + useCache: false, // handled in osparc.store.Pricing endpoints: { getOne: { method: "GET", @@ -918,7 +918,11 @@ qx.Class.define("osparc.data.Resources", { putAutoRecharge: { method: "PUT", url: statics.API + "/wallets/{walletId}/auto-recharge" - } + }, + purchases: { + method: "GET", + url: statics.API + "/wallets/{walletId}/licensed-items-purchases" + }, } }, /* @@ -1247,6 +1251,27 @@ qx.Class.define("osparc.data.Resources", { url: statics.API + "/tags/{tagId}" } } + }, + + /* + * LICENSED ITEMS + */ + "licensedItems": { + useCache: true, + endpoints: { + get: { + method: "GET", + url: statics.API + "/catalog/licensed-items" + }, + getPage: { + method: "GET", + url: statics.API + "/catalog/licensed-items?offset={offset}&limit={limit}" + }, + purchase: { + method: "POST", + url: statics.API + "/catalog/licensed-items/{licensedItemId}:purchase" + }, + } } }; }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/PricingPlan.js b/services/static-webserver/client/source/class/osparc/data/model/PricingPlan.js new file mode 100644 index 00000000000..b6fc4031552 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/model/PricingPlan.js @@ -0,0 +1,99 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +/** + * Class that stores PricingPlan data. + */ + +qx.Class.define("osparc.data.model.PricingPlan", { + extend: qx.core.Object, + + /** + * @param pricingPlanData {Object} Object containing the serialized PricingPlan Data + */ + construct: function(pricingPlanData) { + this.base(arguments); + + this.set({ + pricingPlanId: pricingPlanData.pricingPlanId, + pricingPlanKey: pricingPlanData.pricingPlanKey, + classification: pricingPlanData.classification, + name: pricingPlanData.displayName, + description: pricingPlanData.description, + isActive: pricingPlanData.isActive, + pricingUnits: [], + }); + + if (pricingPlanData.pricingUnits) { + pricingPlanData.pricingUnits.forEach(pricingUnitData => { + const pricingUnit = new osparc.data.model.PricingUnit(pricingUnitData); + this.getPricingUnits().push(pricingUnit); + }); + } + }, + + properties: { + pricingPlanId: { + check: "Number", + nullable: false, + init: null, + event: "changePricingPlanId" + }, + + pricingPlanKey: { + check: "String", + nullable: true, + init: null, + event: "changePricingPlanKey" + }, + + pricingUnits: { + check: "Array", + nullable: true, + init: [], + event: "changePricingunits" + }, + + classification: { + check: ["TIER", "LICENSE"], + nullable: false, + init: null, + event: "changeClassification" + }, + + name: { + check: "String", + nullable: false, + init: null, + event: "changeName" + }, + + description: { + check: "String", + nullable: true, + init: null, + event: "changeDescription" + }, + + isActive: { + check: "Boolean", + nullable: false, + init: false, + event: "changeIsActive" + }, + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/data/model/PricingUnit.js b/services/static-webserver/client/source/class/osparc/data/model/PricingUnit.js new file mode 100644 index 00000000000..91970b854af --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/model/PricingUnit.js @@ -0,0 +1,91 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +/** + * Class that stores PricingUnit data. + */ + +qx.Class.define("osparc.data.model.PricingUnit", { + extend: qx.core.Object, + + /** + * @param pricingUnitData {Object} Object containing the serialized PricingUnit Data + */ + construct: function(pricingUnitData) { + this.base(arguments); + + this.set({ + pricingUnitId: pricingUnitData.pricingUnitId, + name: pricingUnitData.unitName, + cost: parseFloat(pricingUnitData.currentCostPerUnit), + isDefault: pricingUnitData.default, + extraInfo: pricingUnitData.unitExtraInfo, + specificInfo: pricingUnitData.specificInfo || null, + }); + }, + + properties: { + pricingUnitId: { + check: "Number", + nullable: true, + init: null, + event: "changePricingUnitId" + }, + + classification: { + check: ["TIER", "LICENSE"], + nullable: false, + init: null, + event: "changeClassification" + }, + + name: { + check: "String", + nullable: false, + init: null, + event: "changeName" + }, + + cost: { + check: "Number", + nullable: false, + init: null, + event: "changeCost" + }, + + isDefault: { + check: "Boolean", + nullable: false, + init: false, + event: "changeIsDefault", + }, + + extraInfo: { + check: "Object", + nullable: false, + init: null, + event: "changeExtraInfo", + }, + + specificInfo: { + check: "Object", + nullable: true, + init: null, + event: "changeSpecificInfo", + }, + }, +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js index d6df7d06b28..4dfad42c6e9 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -752,7 +752,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { __iFrameChanged: function(node) { this.__iframePage.removeAll(); - if (node) { + if (node && node.getIFrame()) { const loadingPage = node.getLoadingPage(); const iFrame = node.getIFrame(); const src = iFrame.getSource(); diff --git a/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js b/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js index 160ab65ae29..b96841de7d9 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js +++ b/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js @@ -112,13 +112,14 @@ qx.Class.define("osparc.navigation.UserMenu", { this.add(control); break; } - case "license": + case "license": { control = new qx.ui.menu.Button(this.tr("License")); osparc.utils.Utils.setIdToWidget(control, "userMenuLicenseBtn"); const licenseURL = osparc.store.Support.getLicenseURL(); control.addListener("execute", () => window.open(licenseURL)); this.add(control); break; + } case "tip-lite-button": control = new qx.ui.menu.Button(this.tr("Access Full TIP")); osparc.utils.Utils.setIdToWidget(control, "userMenuAccessTIPBtn"); @@ -237,7 +238,7 @@ qx.Class.define("osparc.navigation.UserMenu", { this.addSeparator(); this.__addAnnouncements(); - + if (osparc.product.Utils.showS4LStore()) { this.getChildControl("market"); } diff --git a/services/static-webserver/client/source/class/osparc/node/TierSelectionView.js b/services/static-webserver/client/source/class/osparc/node/TierSelectionView.js index ffa1431a00e..f23b6077499 100644 --- a/services/static-webserver/client/source/class/osparc/node/TierSelectionView.js +++ b/services/static-webserver/client/source/class/osparc/node/TierSelectionView.js @@ -46,22 +46,20 @@ qx.Class.define("osparc.node.TierSelectionView", { tiersLayout.add(tierBox); const node = this.getNode(); - const plansParams = { - url: osparc.data.Resources.getServiceUrl( - node.getKey(), - node.getVersion() - ) - }; - const studyId = node.getStudy().getUuid(); - const nodeId = node.getNodeId(); - osparc.data.Resources.fetch("services", "pricingPlans", plansParams) + const pricingStore = osparc.store.Pricing.getInstance(); + pricingStore.fetchPricingPlansService(node.getKey(), node.getVersion()) .then(pricingPlans => { if (pricingPlans && "pricingUnits" in pricingPlans && pricingPlans["pricingUnits"].length) { - const pUnits = pricingPlans["pricingUnits"]; - pUnits.forEach(pUnit => { - const tItem = new qx.ui.form.ListItem(pUnit.unitName, null, pUnit.pricingUnitId); + const pricingUnits = pricingPlans["pricingUnits"].map(princingUnitData => { + const pricingUnit = new osparc.data.model.PricingUnit(princingUnitData); + return pricingUnit; + }); + pricingUnits.forEach(pricingUnit => { + const tItem = new qx.ui.form.ListItem(pricingUnit.getName(), null, pricingUnit.getPricingUnitId()); tierBox.add(tItem); }); + const studyId = node.getStudy().getUuid(); + const nodeId = node.getNodeId(); const unitParams = { url: { studyId, @@ -81,9 +79,9 @@ qx.Class.define("osparc.node.TierSelectionView", { }) .finally(() => { const pUnitUIs = []; - pUnits.forEach(pUnit => { - const pUnitUI = new osparc.study.PricingUnit(pUnit).set({ - allowGrowX: false + pricingUnits.forEach(pricingUnit => { + const pUnitUI = new osparc.study.PricingUnitTier(pricingUnit).set({ + showEditButton: false, }); pUnitUI.getChildControl("name").exclude(); pUnitUI.exclude(); diff --git a/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js b/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js index ce84b75556d..4aeb8b0b4a6 100644 --- a/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js +++ b/services/static-webserver/client/source/class/osparc/node/slideshow/NodeView.js @@ -143,7 +143,7 @@ qx.Class.define("osparc.node.slideshow.NodeView", { this._iFrameLayout.removeAll(); const node = this.getNode(); - if (node) { + if (node && node.getIFrame()) { const loadingPage = node.getLoadingPage(); const iFrame = node.getIFrame(); const src = iFrame.getSource(); diff --git a/services/static-webserver/client/source/class/osparc/pricing/PlanData.js b/services/static-webserver/client/source/class/osparc/pricing/PlanData.js deleted file mode 100644 index d2c3e00275c..00000000000 --- a/services/static-webserver/client/source/class/osparc/pricing/PlanData.js +++ /dev/null @@ -1,89 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2024 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -/** - * Class that stores Pricing Plan data. - * - */ - -qx.Class.define("osparc.pricing.PlanData", { - extend: qx.core.Object, - - construct: function(planData) { - this.base(arguments); - - this.set({ - pricingPlanId: planData.pricingPlanId, - pricingPlanKey: planData.pricingPlanKey, - displayName: planData.displayName, - description: planData.description, - classification: planData.classification, - isActive: planData.isActive - }); - }, - - properties: { - pricingPlanId: { - check: "Number", - nullable: false, - init: 0, - event: "changePricingPlanId" - }, - - pricingPlanKey: { - check: "String", - nullable: false, - init: "", - event: "changePricingPlanKey" - }, - - displayName: { - check: "String", - init: "", - nullable: false, - event: "changeDisplayName" - }, - - description: { - check: "String", - init: "", - nullable: false, - event: "changeDescription" - }, - - classification: { - check: "String", - init: "TIER", - nullable: false, - event: "changeClassification" - }, - - isActive: { - check: "Boolean", - init: true, - nullable: false, - event: "changeIsActive" - }, - - pricingUnits: { - check: "Array", - init: [], - nullable: false, - event: "changePricingUnits" - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/pricing/PlanEditor.js b/services/static-webserver/client/source/class/osparc/pricing/PlanEditor.js index c638343ed88..50543ee77e5 100644 --- a/services/static-webserver/client/source/class/osparc/pricing/PlanEditor.js +++ b/services/static-webserver/client/source/class/osparc/pricing/PlanEditor.js @@ -38,11 +38,11 @@ qx.Class.define("osparc.pricing.PlanEditor", { if (pricingPlan) { this.__pricingPlan = osparc.utils.Utils.deepCloneObject(pricingPlan); this.set({ - ppKey: pricingPlan.pricingPlanKey, - name: pricingPlan.displayName, - description: pricingPlan.description, - classification: pricingPlan.classification, - isActive: pricingPlan.isActive + ppKey: pricingPlan.getPricingPlanKey(), + name: pricingPlan.getName(), + description: pricingPlan.getDescription(), + classification: pricingPlan.getClassification(), + isActive: pricingPlan.getIsActive(), }); ppKey.setEnabled(false); this.getChildControl("save"); @@ -75,8 +75,8 @@ qx.Class.define("osparc.pricing.PlanEditor", { }, classification: { - check: "String", - init: "TIER", + check: ["TIER", "LICENSE"], + init: "", nullable: false, event: "changeClassification" }, @@ -132,12 +132,21 @@ qx.Class.define("osparc.pricing.PlanEditor", { break; } case "classification": { - control = new qx.ui.form.TextField().set({ + control = new qx.ui.form.SelectBox().set({ font: "text-14", - enabled: false + }); + [ + "TIER", + "LICENSE", + ].forEach(c => { + const cItem = new qx.ui.form.ListItem(c); + control.add(cItem); }); this.bind("classification", control, "value"); - control.bind("value", this, "classification"); + control.addListener("changeValue", e => { + const currentSelection = e.getData(); + this.setClassification(currentSelection.getLabel()); + }, this); this._add(control); break; } @@ -200,43 +209,41 @@ qx.Class.define("osparc.pricing.PlanEditor", { const name = this.getName(); const description = this.getDescription(); const classification = this.getClassification(); - const params = { - data: { - "pricingPlanKey": ppKey, - "displayName": name, - "description": description, - "classification": classification - } + const newPricingPlanData = { + "pricingPlanKey": ppKey, + "displayName": name, + "description": description, + "classification": classification, }; - osparc.data.Resources.fetch("pricingPlans", "post", params) + osparc.store.Pricing.getInstance().postPricingPlan(newPricingPlanData) .then(() => { osparc.FlashMessenger.getInstance().logAs(name + this.tr(" successfully created")); this.fireEvent("done"); }) .catch(err => { - osparc.FlashMessenger.getInstance().logAs(this.tr("Something went wrong creating ") + name, "ERROR"); + const errorMsg = err.message || this.tr("Something went wrong creating ") + name; + osparc.FlashMessenger.getInstance().logAs(errorMsg, "ERROR"); console.error(err); }) .finally(() => this.getChildControl("create").setFetching(false)); }, __updatePricingPlan: function() { - this.__pricingPlan["displayName"] = this.getName(); - this.__pricingPlan["description"] = this.getDescription(); - this.__pricingPlan["isActive"] = this.getIsActive(); - const params = { - url: { - "pricingPlanId": this.__pricingPlan["pricingPlanId"] - }, - data: this.__pricingPlan + const updateData = { + "pricingPlanKey": this.getPpKey(), + "displayName": this.getName(), + "description": this.getDescription(), + "classification": this.getClassification(), + "isActive": this.getIsActive(), }; - osparc.data.Resources.fetch("pricingPlans", "update", params) + osparc.store.Pricing.getInstance().putPricingPlan(this.__pricingPlan["pricingPlanId"], updateData) .then(() => { osparc.FlashMessenger.getInstance().logAs(this.tr("Successfully updated")); this.fireEvent("done"); }) .catch(err => { - osparc.FlashMessenger.getInstance().logAs(this.tr("Something went wrong"), "ERROR"); + const errorMsg = err.message || this.tr("Something went wrong"); + osparc.FlashMessenger.getInstance().logAs(errorMsg, "ERROR"); console.error(err); }) .finally(() => this.getChildControl("save").setFetching(false)); diff --git a/services/static-webserver/client/source/class/osparc/pricing/PlanListItem.js b/services/static-webserver/client/source/class/osparc/pricing/PlanListItem.js index 522f9e35317..a76527e35fd 100644 --- a/services/static-webserver/client/source/class/osparc/pricing/PlanListItem.js +++ b/services/static-webserver/client/source/class/osparc/pricing/PlanListItem.js @@ -30,6 +30,7 @@ qx.Class.define("osparc.pricing.PlanListItem", { this.getChildControl("title"); this.getChildControl("description"); + this.getChildControl("classification"); this.getChildControl("edit-button"); this.addListener("pointerover", this._onPointerOver, this); @@ -74,6 +75,12 @@ qx.Class.define("osparc.pricing.PlanListItem", { event: "changeDescription" }, + classification: { + check: "String", + nullable: true, + event: "changeClassification" + }, + isActive: { check: "Boolean", apply: "__applyIsActive", @@ -112,7 +119,8 @@ qx.Class.define("osparc.pricing.PlanListItem", { case "pp-id": control = new qx.ui.basic.Label().set({ font: "text-14", - alignY: "middle" + alignY: "middle", + width: 35, }); this._add(control, { row: 0, @@ -123,7 +131,8 @@ qx.Class.define("osparc.pricing.PlanListItem", { case "pp-key": control = new qx.ui.basic.Label().set({ font: "text-14", - alignY: "middle" + alignY: "middle", + width: 80, }); this._add(control, { row: 0, @@ -151,6 +160,19 @@ qx.Class.define("osparc.pricing.PlanListItem", { column: 2 }); break; + case "classification": + control = new qx.ui.basic.Label().set({ + font: "text-14", + alignY: "middle", + width: 60, + }); + this.bind("classification", control, "value"); + this._add(control, { + row: 0, + column: 3, + rowSpan: 2 + }); + break; case "is-active": control = new qx.ui.basic.Label().set({ font: "text-14", @@ -158,7 +180,7 @@ qx.Class.define("osparc.pricing.PlanListItem", { }); this._add(control, { row: 0, - column: 3, + column: 4, rowSpan: 2 }); break; @@ -170,7 +192,7 @@ qx.Class.define("osparc.pricing.PlanListItem", { control.addListener("tap", () => this.fireEvent("editPricingPlan")); this._add(control, { row: 0, - column: 4, + column: 5, rowSpan: 2 }); break; @@ -206,7 +228,7 @@ qx.Class.define("osparc.pricing.PlanListItem", { return; } const label = this.getChildControl("is-active"); - label.setValue("Active: " + value); + label.setValue(value ? "Active" : "Inactive"); }, /** diff --git a/services/static-webserver/client/source/class/osparc/pricing/Plans.js b/services/static-webserver/client/source/class/osparc/pricing/Plans.js index d539698f8d8..7067d418e45 100644 --- a/services/static-webserver/client/source/class/osparc/pricing/Plans.js +++ b/services/static-webserver/client/source/class/osparc/pricing/Plans.js @@ -91,8 +91,9 @@ qx.Class.define("osparc.pricing.Plans", { ctrl.bindProperty("pricingPlanId", "model", null, item, id); ctrl.bindProperty("pricingPlanId", "ppId", null, item, id); ctrl.bindProperty("pricingPlanKey", "ppKey", null, item, id); - ctrl.bindProperty("displayName", "title", null, item, id); + ctrl.bindProperty("name", "title", null, item, id); ctrl.bindProperty("description", "description", null, item, id); + ctrl.bindProperty("classification", "classification", null, item, id); ctrl.bindProperty("isActive", "isActive", null, item, id); }, configureItem: item => { @@ -103,13 +104,13 @@ qx.Class.define("osparc.pricing.Plans", { }, fetchPlans: function() { - osparc.data.Resources.fetch("pricingPlans", "get") + osparc.store.Pricing.getInstance().fetchPricingPlans() .then(data => this.__populateList(data)); }, __populateList: function(pricingPlans) { this.__model.removeAll(); - pricingPlans.forEach(pricingPlan => this.__model.append(new osparc.pricing.PlanData(pricingPlan))); + pricingPlans.forEach(pricingPlan => this.__model.append(pricingPlan)); }, __openCreatePricingPlan: function() { @@ -124,12 +125,7 @@ qx.Class.define("osparc.pricing.Plans", { }, __openUpdatePricingPlan: function(pricingPlanId) { - const params = { - url: { - pricingPlanId - } - } - osparc.data.Resources.fetch("pricingPlans", "getOne", params) + osparc.store.Pricing.getInstance().fetchPricingUnits(pricingPlanId) .then(pricingPlan => { const ppEditor = new osparc.pricing.PlanEditor(pricingPlan); const title = this.tr("Pricing Plan Editor"); diff --git a/services/static-webserver/client/source/class/osparc/pricing/UnitData.js b/services/static-webserver/client/source/class/osparc/pricing/UnitData.js deleted file mode 100644 index 7f636ee9ab8..00000000000 --- a/services/static-webserver/client/source/class/osparc/pricing/UnitData.js +++ /dev/null @@ -1,90 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2024 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -/** - * Class that stores Pricing Unit data. - * - */ - -qx.Class.define("osparc.pricing.UnitData", { - extend: qx.core.Object, - - construct: function(unitData) { - this.base(arguments); - - this.set({ - pricingUnitId: unitData.pricingUnitId ? unitData.pricingUnitId : null, - unitName: unitData.unitName, - currentCostPerUnit: parseFloat(unitData.currentCostPerUnit), - comment: unitData.comment ? unitData.comment : "", - awsSpecificInfo: unitData.specificInfo && unitData.specificInfo["aws_ec2_instances"] ? unitData.specificInfo["aws_ec2_instances"].toString() : "", - unitExtraInfo: unitData.unitExtraInfo, - default: unitData.default - }); - }, - - properties: { - pricingUnitId: { - check: "Number", - nullable: true, - init: null, - event: "changePricingUnitId" - }, - - unitName: { - check: "String", - init: "", - nullable: false, - event: "changeUnitName" - }, - - currentCostPerUnit: { - check: "Number", - nullable: false, - init: 0, - event: "changeCurrentCostPerUnit" - }, - - comment: { - check: "String", - init: "", - nullable: false, - event: "changeComment" - }, - - awsSpecificInfo: { - check: "String", - init: "", - nullable: false, - event: "changeAwsSpecificInfo" - }, - - unitExtraInfo: { - check: "Object", - init: {}, - nullable: false, - event: "changeUnitExtraInfo" - }, - - default: { - check: "Boolean", - init: true, - nullable: false, - event: "changeDefault" - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/pricing/UnitEditor.js b/services/static-webserver/client/source/class/osparc/pricing/UnitEditor.js index a03fdc64d71..26469666570 100644 --- a/services/static-webserver/client/source/class/osparc/pricing/UnitEditor.js +++ b/services/static-webserver/client/source/class/osparc/pricing/UnitEditor.js @@ -18,7 +18,7 @@ qx.Class.define("osparc.pricing.UnitEditor", { extend: qx.ui.core.Widget, - construct: function(pricingUnitData) { + construct: function(pricingUnit) { this.base(arguments); this._setLayout(new qx.ui.layout.VBox(10)); @@ -46,28 +46,31 @@ qx.Class.define("osparc.pricing.UnitEditor", { manager.add(specificInfo); manager.add(unitExtraInfo); - if (pricingUnitData) { + if (pricingUnit) { this.set({ - pricingUnitId: pricingUnitData.pricingUnitId, - unitName: pricingUnitData.unitName, - costPerUnit: parseFloat(pricingUnitData.currentCostPerUnit), - comment: pricingUnitData.comment ? pricingUnitData.comment : "", - specificInfo: pricingUnitData.specificInfo && pricingUnitData.specificInfo["aws_ec2_instances"] ? pricingUnitData.specificInfo["aws_ec2_instances"].toString() : "", - default: pricingUnitData.default - }); - const extraInfo = osparc.utils.Utils.deepCloneObject(pricingUnitData.unitExtraInfo); - // extract the required fields from the unitExtraInfo - this.set({ - unitExtraInfoCPU: extraInfo["CPU"], - unitExtraInfoRAM: extraInfo["RAM"], - unitExtraInfoVRAM: extraInfo["VRAM"] - }); - delete extraInfo["CPU"]; - delete extraInfo["RAM"]; - delete extraInfo["VRAM"]; - this.set({ - unitExtraInfo: extraInfo + pricingUnitId: pricingUnit.getPricingUnitId(), + unitName: pricingUnit.getName(), + costPerUnit: pricingUnit.getCost(), }); + if (pricingUnit.getClassification() === "TIER") { + this.set({ + specificInfo: pricingUnit.getSpecificInfo() && pricingUnit.getSpecificInfo()["aws_ec2_instances"] ? pricingUnit.getSpecificInfo()["aws_ec2_instances"].toString() : "", + default: pricingUnit.getIsDefault(), + }); + const extraInfo = osparc.utils.Utils.deepCloneObject(pricingUnit.getExtraInfo()); + // extract the required fields from the unitExtraInfo + this.set({ + unitExtraInfoCPU: extraInfo["CPU"], + unitExtraInfoRAM: extraInfo["RAM"], + unitExtraInfoVRAM: extraInfo["VRAM"] + }); + delete extraInfo["CPU"]; + delete extraInfo["RAM"]; + delete extraInfo["VRAM"]; + this.set({ + unitExtraInfo: extraInfo + }); + } this.getChildControl("save"); } else { this.getChildControl("create"); diff --git a/services/static-webserver/client/source/class/osparc/pricing/UnitsList.js b/services/static-webserver/client/source/class/osparc/pricing/UnitsList.js index c941aee8323..054d4e4f11b 100644 --- a/services/static-webserver/client/source/class/osparc/pricing/UnitsList.js +++ b/services/static-webserver/client/source/class/osparc/pricing/UnitsList.js @@ -43,7 +43,7 @@ qx.Class.define("osparc.pricing.UnitsList", { let control; switch (id) { case "pricing-units-container": - control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); this._addAt(control, 0, { flex: 1 }); @@ -63,13 +63,8 @@ qx.Class.define("osparc.pricing.UnitsList", { }, __fetchUnits: function() { - const params = { - url: { - pricingPlanId: this.getPricingPlanId() - } - }; - osparc.data.Resources.fetch("pricingPlans", "getOne", params) - .then(data => this.__populateList(data["pricingUnits"])); + osparc.store.Pricing.getInstance().fetchPricingUnits(this.getPricingPlanId()) + .then(pricingUnits => this.__populateList(pricingUnits)); }, __populateList: function(pricingUnits) { @@ -80,23 +75,27 @@ qx.Class.define("osparc.pricing.UnitsList", { } pricingUnits.forEach(pricingUnit => { - const pUnit = new osparc.study.PricingUnit(pricingUnit).set({ - showSpecificInfo: true, + let pUnit = null; + if (pricingUnit.getClassification() === "LICENSE") { + pUnit = new osparc.study.PricingUnitLicense(pricingUnit).set({ + showRentButton: false, + }); + } else { + pUnit = new osparc.study.PricingUnitTier(pricingUnit).set({ + showAwsSpecificInfo: true, + }); + } + pUnit.set({ showEditButton: true, - allowGrowY: false }); pUnit.addListener("editPricingUnit", () => this.__openUpdatePricingUnit(pricingUnit)); this.getChildControl("pricing-units-container").add(pUnit); }); const buttons = this.getChildControl("pricing-units-container").getChildren(); - const keepDefaultSelected = () => { - buttons.forEach(btn => { - btn.setValue(btn.getUnitData().isDefault()); - }); - }; - keepDefaultSelected(); - buttons.forEach(btn => btn.addListener("execute", () => keepDefaultSelected())); + buttons.forEach(btn => { + btn.setSelected(btn.getUnitData().getIsDefault()); + }); }, __openCreatePricingUnit: function() { diff --git a/services/static-webserver/client/source/class/osparc/service/PricingUnitsList.js b/services/static-webserver/client/source/class/osparc/service/PricingUnitsList.js index f7dcc85a457..215b17d935b 100644 --- a/services/static-webserver/client/source/class/osparc/service/PricingUnitsList.js +++ b/services/static-webserver/client/source/class/osparc/service/PricingUnitsList.js @@ -47,13 +47,8 @@ qx.Class.define("osparc.service.PricingUnitsList", { }, __fetchUnits: function() { - const plansParams = { - url: osparc.data.Resources.getServiceUrl( - this.__serviceMetadata["key"], - this.__serviceMetadata["version"] - ) - }; - osparc.data.Resources.fetch("services", "pricingPlans", plansParams) + const pricingStore = osparc.store.Pricing.getInstance(); + pricingStore.fetchPricingPlansService(this.__serviceMetadata["key"], this.__serviceMetadata["version"]) .then(data => this.__populateList(data["pricingUnits"])) .catch(err => { console.error(err); @@ -61,11 +56,11 @@ qx.Class.define("osparc.service.PricingUnitsList", { }); }, - __populateList: function(pricingUnits) { + __populateList: function(pricingUnitsData) { this.getChildControl("pricing-units-container").removeAll(); - if (pricingUnits.length) { - const pUnits = new osparc.study.PricingUnits(pricingUnits, null, false); + if (pricingUnitsData.length) { + const pUnits = new osparc.study.PricingUnitTiers(pricingUnitsData, null, false); this.getChildControl("pricing-units-container").add(pUnits); } else { const notFound = new qx.ui.basic.Label().set({ diff --git a/services/static-webserver/client/source/class/osparc/store/Pricing.js b/services/static-webserver/client/source/class/osparc/store/Pricing.js new file mode 100644 index 00000000000..08d01f9e3c8 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/store/Pricing.js @@ -0,0 +1,164 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.store.Pricing", { + extend: qx.core.Object, + type: "singleton", + + construct: function() { + this.base(arguments); + + this.pricingPlansCached = []; + }, + + events: { + "pricingPlansChanged": "qx.event.type.Data", + }, + + members: { + pricingPlansCached: null, + + fetchPricingPlans: function() { + return osparc.data.Resources.fetch("pricingPlans", "get") + .then(pricingPlansData => { + const pricingPlans = []; + pricingPlansData.forEach(pricingPlanData => { + const pricingPlan = this.__addToCache(pricingPlanData); + pricingPlans.push(pricingPlan); + }); + return pricingPlans; + }); + }, + + postPricingPlan: function(newPricingPlanData) { + const params = { + data: newPricingPlanData + }; + return osparc.data.Resources.fetch("pricingPlans", "post", params) + .then(pricingPlanData => { + const pricingPlan = this.__addToCache(pricingPlanData); + this.fireDataEvent("pricingPlansChanged", pricingPlan); + return pricingPlan; + }); + }, + + putPricingPlan: function(pricingPlanId, updateData) { + const params = { + url: { + pricingPlanId + }, + data: updateData + }; + return osparc.data.Resources.getInstance().fetch("pricingPlans", "update", params) + .then(pricingPlanData => { + return this.__addToCache(pricingPlanData); + }) + .catch(console.error); + }, + + fetchPricingPlansService: function(serviceKey, serviceVersion) { + const plansParams = { + url: osparc.data.Resources.getServiceUrl(serviceKey, serviceVersion) + }; + return osparc.data.Resources.fetch("services", "pricingPlans", plansParams) + .then(pricingPlansData => { + return pricingPlansData; + }); + }, + + fetchPricingUnits: function(pricingPlanId) { + const params = { + url: { + pricingPlanId, + } + }; + return osparc.data.Resources.fetch("pricingPlans", "getOne", params) + .then(pricingPlanData => { + const pricingPlan = this.__addToCache(pricingPlanData); + const pricingUnits = pricingPlan.getPricingUnits(); + pricingUnits.length = 0; + pricingPlanData["pricingUnits"].forEach(pricingUnitData => { + this.__addPricingUnitToCache(pricingPlan, pricingUnitData); + }); + return pricingUnits; + }); + }, + + getPricingPlans: function() { + return this.pricingPlansCached; + }, + + getPricingPlan: function(pricingPlanId = null) { + return this.pricingPlansCached.find(f => f.getPricingPlanId() === pricingPlanId); + }, + + getPricingUnits: function(pricingPlanId) { + const pricingPlan = this.getPricingPlan(pricingPlanId); + if (pricingPlan) { + return pricingPlan.getPricingUnits(); + } + return null; + }, + + getPricingUnit: function(pricingPlanId, pricingUnitId) { + const pricingPlan = this.getPricingPlan(pricingPlanId); + if (pricingPlan) { + return pricingPlan.getPricingUnits().find(pricingUnit => pricingUnit.getPricingUnitId() === pricingUnitId); + } + return null; + }, + + __addToCache: function(pricingPlanData) { + let pricingPlan = this.pricingPlansCached.find(f => f.getPricingPlanId() === pricingPlanData["pricingPlanId"]); + if (pricingPlan) { + // put + pricingPlan.set({ + pricingPlanKey: pricingPlanData["pricingPlanKey"], + name: pricingPlanData["displayName"], + description: pricingPlanData["description"], + classification: pricingPlanData["classification"], + isActive: pricingPlanData["isActive"], + }); + } else { + // get and post + pricingPlan = new osparc.data.model.PricingPlan(pricingPlanData); + this.pricingPlansCached.unshift(pricingPlan); + } + return pricingPlan; + }, + + __addPricingUnitToCache: function(pricingPlan, pricingUnitData) { + const pricingUnits = pricingPlan.getPricingUnits(); + let pricingUnit = pricingUnits ? pricingUnits.find(unit => ("getPricingUnitId" in unit) && unit.getPricingUnitId() === pricingUnitData["pricingUnitId"]) : null; + if (pricingUnit) { + const props = Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.PricingPlan)); + // put + Object.keys(pricingUnitData).forEach(key => { + if (props.includes(key)) { + pricingPlan.set(key, pricingUnitData[key]); + } + }); + } else { + // get and post + pricingUnit = new osparc.data.model.PricingUnit(pricingUnitData); + pricingPlan.bind("classification", pricingUnit, "classification"); + pricingUnits.push(pricingUnit); + } + return pricingUnit; + }, + } +}); 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 6b986a0a34d..ea05e789754 100644 --- a/services/static-webserver/client/source/class/osparc/store/Store.js +++ b/services/static-webserver/client/source/class/osparc/store/Store.js @@ -221,7 +221,11 @@ qx.Class.define("osparc.store.Store", { tasks: { check: "Array", init: [] - } + }, + market: { + check: "Array", + init: [] + }, }, members: { diff --git a/services/static-webserver/client/source/class/osparc/study/NodePricingUnits.js b/services/static-webserver/client/source/class/osparc/study/NodePricingUnits.js index 76918e12b3e..f6bc409fb39 100644 --- a/services/static-webserver/client/source/class/osparc/study/NodePricingUnits.js +++ b/services/static-webserver/client/source/class/osparc/study/NodePricingUnits.js @@ -100,15 +100,10 @@ qx.Class.define("osparc.study.NodePricingUnits", { const studyId = this.getStudyId(); const nodeId = this.getNodeId(); - const plansParams = { - url: osparc.data.Resources.getServiceUrl( - nodeKey, - nodeVersion - ) - }; - osparc.data.Resources.fetch("services", "pricingPlans", plansParams) - .then(pricingPlan => { - if (pricingPlan) { + const pricingStore = osparc.store.Pricing.getInstance(); + pricingStore.fetchPricingPlansService(nodeKey, nodeVersion) + .then(pricingPlanData => { + if (pricingPlanData) { const unitParams = { url: { studyId, @@ -116,26 +111,32 @@ qx.Class.define("osparc.study.NodePricingUnits", { } }; this.set({ - pricingPlanId: pricingPlan["pricingPlanId"] + pricingPlanId: pricingPlanData["pricingPlanId"] }); osparc.data.Resources.fetch("studies", "getPricingUnit", unitParams) .then(preselectedPricingUnit => { - if (pricingPlan && "pricingUnits" in pricingPlan && pricingPlan["pricingUnits"].length) { - const pricingUnitButtons = this.__pricingUnits = new osparc.study.PricingUnits(pricingPlan["pricingUnits"], preselectedPricingUnit); + if (pricingPlanData && "pricingUnits" in pricingPlanData && pricingPlanData["pricingUnits"].length) { + const pricingUnitsData = pricingPlanData["pricingUnits"]; + const pricingUnitTiers = this.__pricingUnits = new osparc.study.PricingUnitTiers(pricingUnitsData, preselectedPricingUnit); if (inGroupBox) { const pricingUnitsLayout = osparc.study.StudyOptions.createGroupBox(nodeLabel); - pricingUnitsLayout.add(pricingUnitButtons); + pricingUnitsLayout.add(pricingUnitTiers); this._add(pricingUnitsLayout); } else { - this._add(pricingUnitButtons); + this._add(pricingUnitTiers); } - pricingUnitButtons.addListener("changeSelectedUnitId", e => { + pricingUnitTiers.addListener("selectPricingUnitRequested", e => { + const selectedPricingUnitId = e.getData(); if (this.isPatchNode()) { - pricingUnitButtons.setEnabled(false); + pricingUnitTiers.setEnabled(false); const pricingPlanId = this.getPricingPlanId(); - const selectedPricingUnitId = e.getData(); this.self().patchPricingUnitSelection(studyId, nodeId, pricingPlanId, selectedPricingUnitId) - .finally(() => pricingUnitButtons.setEnabled(true)); + .then(() => pricingUnitTiers.setSelectedUnitId(selectedPricingUnitId)) + .catch(err => { + const msg = err.message || this.tr("Cannot change Tier"); + osparc.FlashMessenger.getInstance().logAs(msg, "ERROR"); + }) + .finally(() => pricingUnitTiers.setEnabled(true)); } }); } diff --git a/services/static-webserver/client/source/class/osparc/study/PricingUnit.js b/services/static-webserver/client/source/class/osparc/study/PricingUnit.js index 311df5ea16b..f257af307d5 100644 --- a/services/static-webserver/client/source/class/osparc/study/PricingUnit.js +++ b/services/static-webserver/client/source/class/osparc/study/PricingUnit.js @@ -16,42 +16,50 @@ ************************************************************************ */ qx.Class.define("osparc.study.PricingUnit", { - extend: qx.ui.form.ToggleButton, + extend: qx.ui.core.Widget, + type: "abstract", construct: function(pricingUnit) { this.base(arguments); + this._setLayout(new qx.ui.layout.VBox(5)); + this.set({ padding: 10, - center: true, decorator: "rounded", + minWidth: 100, + allowGrowX: false, + allowGrowY: false, }); - this.setUnitData(new osparc.pricing.UnitData(pricingUnit)); + this.setUnitData(pricingUnit); + + osparc.utils.Utils.addBorder(this); }, events: { - "editPricingUnit": "qx.event.type.Event" + "editPricingUnit": "qx.event.type.Event", }, properties: { - unitData: { - check: "osparc.pricing.UnitData", + selected: { + check: "Boolean", + init: false, nullable: false, - init: null, - apply: "__buildLayout" + event: "changeSelected", + apply: "__applySelected", }, - showSpecificInfo: { - check: "Boolean", + unitData: { + check: "osparc.data.model.PricingUnit", + nullable: false, init: null, - nullable: true, - event: "changeShowSpecificInfo" + apply: "_buildLayout" }, showEditButton: { check: "Boolean", - init: null, + init: false, nullable: true, event: "changeShowEditButton" }, @@ -73,58 +81,28 @@ qx.Class.define("osparc.study.PricingUnit", { }); this._add(control); break; - case "awsSpecificInfo": - control = new qx.ui.basic.Label().set({ - font: "text-14" - }); - this._add(control); - break; case "edit-button": control = new qx.ui.form.Button(qx.locale.Manager.tr("Edit")); + this.bind("showEditButton", control, "visibility", { + converter: show => show ? "visible" : "excluded" + }); + control.addListener("execute", () => this.fireEvent("editPricingUnit")); this._add(control); break; } return control || this.base(arguments, id); }, - __buildLayout: function(pricingUnit) { + _buildLayout: function(pricingUnit) { this._removeAll(); - this._setLayout(new qx.ui.layout.VBox(5)); - - const unitName = this.getChildControl("name"); - pricingUnit.bind("unitName", unitName, "value"); - - // add price info - const price = this.getChildControl("price"); - pricingUnit.bind("currentCostPerUnit", price, "value", { - converter: v => qx.locale.Manager.tr("Credits/h") + ": " + v, - }); - - // add aws specific info - if ("specificInfo" in pricingUnit) { - const specificInfo = this.getChildControl("awsSpecificInfo"); - pricingUnit.bind("awsSpecificInfo", specificInfo, "value", { - converter: v => qx.locale.Manager.tr("EC2") + ": " + v, - }); - this.bind("showSpecificInfo", specificInfo, "visibility", { - converter: show => show ? "visible" : "excluded" - }) - } - // add pricing unit extra info - Object.entries(pricingUnit.getUnitExtraInfo()).forEach(([key, value]) => { - this._add(new qx.ui.basic.Label().set({ - value: key + ": " + value, - font: "text-13" - })); - }); - - // add edit button - const editButton = this.getChildControl("edit-button"); - this.bind("showEditButton", editButton, "visibility", { - converter: show => show ? "visible" : "excluded" - }) - editButton.addListener("execute", () => this.fireEvent("editPricingUnit")); - } + const name = this.getChildControl("name"); + pricingUnit.bind("name", name, "value"); + }, + + __applySelected: function(selected) { + const strong = qx.theme.manager.Color.getInstance().resolve("strong-main"); + osparc.utils.Utils.updateBorderColor(this, selected ? strong : "transparent"); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/study/PricingUnitLicense.js b/services/static-webserver/client/source/class/osparc/study/PricingUnitLicense.js new file mode 100644 index 00000000000..d7fd9f04d27 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/study/PricingUnitLicense.js @@ -0,0 +1,70 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.study.PricingUnitLicense", { + extend: osparc.study.PricingUnit, + + events: { + "rentPricingUnit": "qx.event.type.Event", + }, + + properties: { + showRentButton: { + check: "Boolean", + init: false, + nullable: true, + event: "changeShowRentButton" + }, + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "rent-button": + control = new qx.ui.form.Button(qx.locale.Manager.tr("Rent")).set({ + appearance: "strong-button", + center: true, + }); + this.bind("showRentButton", control, "visibility", { + converter: show => show ? "visible" : "excluded" + }); + control.addListener("execute", () => this.fireEvent("rentPricingUnit")); + this._add(control); + break; + } + return control || this.base(arguments, id); + }, + + // override + _buildLayout: function(pricingUnit) { + this.base(arguments, pricingUnit); + + // add price info + const price = this.getChildControl("price"); + pricingUnit.bind("cost", price, "value", { + converter: v => qx.locale.Manager.tr("Credits") + ": " + v + }); + + // add edit button + this.getChildControl("edit-button"); + + // add rent button + this.getChildControl("rent-button"); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/study/PricingUnitTier.js b/services/static-webserver/client/source/class/osparc/study/PricingUnitTier.js new file mode 100644 index 00000000000..292a44efabf --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/study/PricingUnitTier.js @@ -0,0 +1,125 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.study.PricingUnitTier", { + extend: osparc.study.PricingUnit, + + events: { + "selectPricingUnit": "qx.event.type.Event", + }, + + properties: { + showAwsSpecificInfo: { + check: "Boolean", + init: false, + nullable: true, + event: "changeShowAwsSpecificInfo" + }, + + showUnitExtraInfo: { + check: "Boolean", + init: true, + nullable: true, + event: "changeShowUnitExtraInfo" + }, + + showSelectButton: { + check: "Boolean", + init: false, + nullable: true, + event: "changeShowSelectButton" + }, + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "awsSpecificInfo": + control = new qx.ui.basic.Label().set({ + font: "text-14" + }); + this.bind("showAwsSpecificInfo", control, "visibility", { + converter: show => show ? "visible" : "excluded" + }) + this._add(control); + break; + case "unitExtraInfo": + control = new qx.ui.basic.Label().set({ + font: "text-13", + rich: true, + }); + this.bind("showUnitExtraInfo", control, "visibility", { + converter: show => show ? "visible" : "excluded" + }); + this._add(control); + break; + case "select-button": + control = new qx.ui.form.Button().set({ + appearance: "strong-button", + center: true, + }); + this.bind("selected", control, "label", { + converter: selected => selected ? "Selected" : "Select" + }); + this.bind("selected", control, "enabled", { + converter: selected => !selected + }); + this.bind("showSelectButton", control, "visibility", { + converter: show => show ? "visible" : "excluded" + }); + control.addListener("execute", () => this.fireEvent("selectPricingUnit")); + this._add(control); + break; + } + return control || this.base(arguments, id); + }, + + // override + _buildLayout: function(pricingUnit) { + this.base(arguments, pricingUnit); + + // add price info + const price = this.getChildControl("price"); + pricingUnit.bind("cost", price, "value", { + converter: v => qx.locale.Manager.tr("Credits/h") + ": " + v + }); + + // add aws specific info + if ("specificInfo" in pricingUnit) { + const specificInfo = this.getChildControl("awsSpecificInfo"); + pricingUnit.bind("awsSpecificInfo", specificInfo, "value", { + converter: v => qx.locale.Manager.tr("EC2") + ": " + v, + }); + } + + // add pricing unit extra info + const unitExtraInfo = this.getChildControl("unitExtraInfo"); + let text = ""; + Object.entries(pricingUnit.getExtraInfo()).forEach(([key, value]) => { + text += `${key}: ${value}
`; + }); + unitExtraInfo.setValue(text); + + // add select button + this.getChildControl("select-button"); + + // add edit button + this.getChildControl("edit-button"); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/study/PricingUnitTiers.js b/services/static-webserver/client/source/class/osparc/study/PricingUnitTiers.js new file mode 100644 index 00000000000..028ff5740ff --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/study/PricingUnitTiers.js @@ -0,0 +1,88 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.study.PricingUnitTiers", { + extend: qx.ui.container.Composite, + + construct: function(pricingUnitsData, preselectedPricingUnit, changeSelectionAllowed = true) { + this.base(arguments); + + this.set({ + layout: new qx.ui.layout.HBox(10), + allowGrowY: false, + }); + + this.__buildLayout(pricingUnitsData, preselectedPricingUnit, changeSelectionAllowed); + }, + + properties: { + selectedUnitId: { + check: "Number", + init: null, + nullable: false, + event: "changeSelectedUnitId", + apply: "__applySelectedUnitId", + } + }, + + events: { + "selectPricingUnitRequested": "qx.event.type.Event", + }, + + members: { + __pricingUnitTiers: null, + + __buildLayout: function(pricingUnitsData, preselectedPricingUnit, changeSelectionAllowed) { + const pricingUnitTiers = this.__pricingUnitTiers = []; + pricingUnitsData.forEach(pricingUnitData => { + const pricingUnit = new osparc.data.model.PricingUnit(pricingUnitData); + const pricingUnitTier = new osparc.study.PricingUnitTier(pricingUnit).set({ + showSelectButton: changeSelectionAllowed, + }); + pricingUnitTiers.push(pricingUnitTier); + this._add(pricingUnitTier); + }); + + if (preselectedPricingUnit) { + const pricingUnitTierFound = pricingUnitTiers.find(pricingUnitTier => pricingUnitTier.getUnitData().getPricingUnitId() === preselectedPricingUnit["pricingUnitId"]); + if (pricingUnitTierFound) { + pricingUnitTierFound.setSelected(true); + } + } else { + // preselect default + pricingUnitTiers.forEach(pricingUnitTier => { + if (pricingUnitTier.getUnitData().getIsDefault()) { + pricingUnitTier.setSelected(true); + } + }); + } + + pricingUnitTiers.forEach(pricingUnitTier => { + pricingUnitTier.addListener("selectPricingUnit", () => { + if (changeSelectionAllowed) { + this.fireDataEvent("selectPricingUnitRequested", pricingUnitTier.getUnitData().getPricingUnitId()); + } + }); + }); + }, + + __applySelectedUnitId: function(selectedUnitId) { + // select and unselect the rest + this.__pricingUnitTiers.forEach(puTIer => puTIer.setSelected(puTIer.getUnitData().getPricingUnitId() === selectedUnitId)); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/study/PricingUnits.js b/services/static-webserver/client/source/class/osparc/study/PricingUnits.js deleted file mode 100644 index 02597e76760..00000000000 --- a/services/static-webserver/client/source/class/osparc/study/PricingUnits.js +++ /dev/null @@ -1,87 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2023 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -qx.Class.define("osparc.study.PricingUnits", { - extend: qx.ui.container.Composite, - - construct: function(pricingUnits, preselectedPricingUnit, changeSelectionAllowed = true) { - this.base(arguments); - - this.set({ - layout: new qx.ui.layout.HBox(5), - allowGrowY: false, - }); - - this.__buildLayout(pricingUnits, preselectedPricingUnit, changeSelectionAllowed); - }, - - properties: { - selectedUnitId: { - check: "Number", - init: null, - nullable: false, - event: "changeSelectedUnitId" - } - }, - - members: { - __buildLayout: function(pricingUnits, preselectedPricingUnit, changeSelectionAllowed) { - const buttons = []; - pricingUnits.forEach(pricingUnit => { - const button = new osparc.study.PricingUnit(pricingUnit); - buttons.push(button); - this._add(button); - }); - - const groupOptions = new qx.ui.form.RadioGroup(); - buttons.forEach(btn => { - groupOptions.add(btn); - btn.bind("value", btn, "backgroundColor", { - converter: selected => selected ? "background-main-1" : "transparent" - }); - }); - - if (preselectedPricingUnit) { - const buttonFound = buttons.find(button => button.getUnitData().getPricingUnitId() === preselectedPricingUnit["pricingUnitId"]); - if (buttonFound) { - buttonFound.setValue(true); - } - } else { - // preselect default - buttons.forEach(button => { - if (button.getUnitData().isDefault()) { - button.setValue(true); - } - }); - } - - buttons.forEach(button => { - if (!changeSelectionAllowed) { - button.setCursor("default"); - } - button.addListener("execute", () => { - if (changeSelectionAllowed) { - const selectedUnitId = button.getUnitData().getPricingUnitId(); - this.setSelectedUnitId(selectedUnitId); - } else { - buttons.forEach(btn => btn.setValue(btn.getUnitData().isDefault())); - } - }); - }); - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js index 40ffda37b27..5e1f87c81ee 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelDetails.js @@ -21,17 +21,25 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { construct: function() { this.base(arguments); - const layout = new qx.ui.layout.Grow(); + const layout = new qx.ui.layout.VBox(15); this._setLayout(layout); this.__poplulateLayout(); }, events: { - "modelLeased": "qx.event.type.Event", + "modelPurchaseRequested": "qx.event.type.Data", + "modelImportRequested": "qx.event.type.Data", }, properties: { + openBy: { + check: "String", + init: null, + nullable: true, + event: "changeOpenBy", + }, + anatomicalModelsData: { check: "Object", init: null, @@ -46,8 +54,12 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { const anatomicalModelsData = this.getAnatomicalModelsData(); if (anatomicalModelsData) { - const card = this.__createcCard(anatomicalModelsData); - this._add(card); + const modelInfo = this.__createModelInfo(anatomicalModelsData); + const pricingUnits = this.__createPricingUnits(anatomicalModelsData); + const importButton = this.__createImportSection(anatomicalModelsData); + this._add(modelInfo); + this._add(pricingUnits); + this._add(importButton); } else { const selectModelLabel = new qx.ui.basic.Label().set({ value: this.tr("Select a model for more details"), @@ -61,13 +73,11 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { } }, - __createcCard: function(anatomicalModelsData) { - console.log(anatomicalModelsData); - + __createModelInfo: function(anatomicalModelsData) { const cardGrid = new qx.ui.layout.Grid(16, 16); const cardLayout = new qx.ui.container.Composite(cardGrid); - const description = anatomicalModelsData["Description"]; + const description = anatomicalModelsData["description"]; description.split(" - ").forEach((desc, idx) => { const titleLabel = new qx.ui.basic.Label().set({ value: desc, @@ -85,7 +95,7 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { }); const thumbnail = new qx.ui.basic.Image().set({ - source: anatomicalModelsData["Thumbnail"], + source: anatomicalModelsData["thumbnail"], alignY: "middle", scale: true, allowGrowX: true, @@ -100,7 +110,7 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { row: 2, }); - const features = anatomicalModelsData["Features"]; + const features = anatomicalModelsData["features"]; const featuresGrid = new qx.ui.layout.Grid(8, 8); const featuresLayout = new qx.ui.container.Composite(featuresGrid); let idx = 0; @@ -125,7 +135,7 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { column: 0, row: idx, }); - + const nameLabel = new qx.ui.basic.Label().set({ value: features[key.toLowerCase()], font: "text-14", @@ -135,7 +145,7 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { column: 1, row: idx, }); - + idx++; } }); @@ -167,40 +177,72 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelDetails", { row: 2, }); - const buttonsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); - if (anatomicalModelsData["leased"]) { - const leaseModelButton = new qx.ui.form.Button().set({ - label: this.tr("3 seats Leased (27 days left)"), - appearance: "strong-button", - center: true, - enabled: false, - }); - buttonsLayout.add(leaseModelButton, { - flex: 1 + return cardLayout; + }, + + __createPricingUnits: function(anatomicalModelsData) { + const pricingUnitsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10).set({ + alignX: "center" + })); + + osparc.store.Pricing.getInstance().fetchPricingUnits(anatomicalModelsData["pricingPlanId"]) + .then(pricingUnits => { + pricingUnits.forEach(pricingUnit => { + pricingUnit.set({ + classification: "LICENSE" + }); + const pUnit = new osparc.study.PricingUnitLicense(pricingUnit).set({ + showRentButton: true, + }); + pUnit.addListener("rentPricingUnit", () => { + this.fireDataEvent("modelPurchaseRequested", { + modelId: anatomicalModelsData["modelId"], + licensedItemId: anatomicalModelsData["licensedItemId"], + pricingPlanId: anatomicalModelsData["pricingPlanId"], + pricingUnitId: pricingUnit.getPricingUnitId(), + }); + }, this); + pricingUnitsLayout.add(pUnit); + }); + }) + .catch(err => console.error(err)); + + return pricingUnitsLayout; + }, + + __createImportSection: function(anatomicalModelsData) { + const importSection = new qx.ui.container.Composite(new qx.ui.layout.VBox(5).set({ + alignX: "center" + })); + + anatomicalModelsData["purchases"].forEach(purchase => { + const seatsText = "seat" + (purchase["numberOfSeats"] > 1 ? "s" : ""); + const entry = new qx.ui.basic.Label().set({ + value: `${purchase["numberOfSeats"]} ${seatsText} available until ${osparc.utils.Utils.formatDate(purchase["expiresAt"])}`, + font: "text-14", }); - } - const leaseModelButton = new osparc.ui.form.FetchButton().set({ - label: this.tr("Lease model (2 for months)"), + importSection.add(entry); + }); + + const importButton = new qx.ui.form.Button().set({ + label: this.tr("Import"), appearance: "strong-button", center: true, + maxWidth: 200, + alignX: "center", }); - leaseModelButton.addListener("execute", () => { - leaseModelButton.setFetching(true); - setTimeout(() => { - leaseModelButton.setFetching(false); - this.fireDataEvent("modelLeased", this.getAnatomicalModelsData()["ID"]); - }, 2000); - }); - buttonsLayout.add(leaseModelButton, { - flex: 1 + this.bind("openBy", importButton, "visibility", { + converter: openBy => openBy ? "visible" : "excluded" }); - cardLayout.add(buttonsLayout, { - column: 0, - row: 3, - colSpan: 2, - }); - - return cardLayout; + importButton.addListener("execute", () => { + this.fireDataEvent("modelImportRequested", { + modelId: anatomicalModelsData["modelId"] + }); + }, this); + if (anatomicalModelsData["purchases"].length) { + importSection.add(importButton); + } + return importSection; }, } }); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js index 30a95396774..75b33a3229d 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/AnatomicalModelListItem.js @@ -57,7 +57,7 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelListItem", { check: "Number", init: null, nullable: false, - event: "changemodelId", + event: "changeModelId", }, thumbnail: { @@ -83,12 +83,26 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelListItem", { event: "changeDate", }, - leased: { - check: "Boolean", - init: false, - nullable: true, - event: "changeLeased", - apply: "__applyLeased", + licensedItemId: { + check: "String", + init: null, + nullable: false, + event: "changeLicensedItemId", + }, + + pricingPlanId: { + check: "Number", + init: null, + nullable: false, + event: "changePricingPlanId", + }, + + purchases: { + check: "Array", + nullable: false, + init: [], + event: "changePurchases", + apply: "__applyPurchases", }, }, @@ -147,8 +161,8 @@ qx.Class.define("osparc.vipMarket.AnatomicalModelListItem", { this.getChildControl("name").setValue(value); }, - __applyLeased: function(value) { - if (value) { + __applyPurchases: function(purchases) { + if (purchases.length) { this.setBackgroundColor("strong-main"); } }, diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/Market.js b/services/static-webserver/client/source/class/osparc/vipMarket/Market.js index dd6a2250c44..8bd65242eb2 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/Market.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/Market.js @@ -26,18 +26,62 @@ qx.Class.define("osparc.vipMarket.Market", { }); this.addWidgetOnTopOfTheTabs(miniWallet); - this.__vipMarketPage = this.__getVipMarketPage(); + osparc.data.Resources.getInstance().getAllPages("licensedItems") + .then(() => { + [{ + category: "human", + label: "Humans", + icon: "@FontAwesome5Solid/users/20", + url: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/HumanWholeBody", + }, { + category: "human_region", + label: "Humans (Region)", + icon: "@FontAwesome5Solid/users/20", + url: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/HumanBodyRegion", + }, { + category: "animal", + label: "Animals", + icon: "@FontAwesome5Solid/users/20", + url: "https://itis.swiss/PD_DirectDownload/getDownloadableItems/AnimalWholeBody", + }, { + category: "phantom", + label: "Phantoms", + icon: "@FontAwesome5Solid/users/20", + url: "https://speag.swiss/PD_DirectDownload/getDownloadableItems/ComputationalPhantom", + }].forEach(marketInfo => { + this.__buildViPMarketPage(marketInfo); + }); + }); }, - members: { - __vipMarketPage: null, + properties: { + openBy: { + check: "String", + init: null, + nullable: true, + event: "changeOpenBy", + }, + }, - __getVipMarketPage: function() { - const title = this.tr("ViP Models"); - const iconSrc = "@FontAwesome5Solid/users/22"; + members: { + __buildViPMarketPage: function(marketInfo) { const vipMarketView = new osparc.vipMarket.VipMarket(); - const page = this.addTab(title, iconSrc, vipMarketView); + vipMarketView.set({ + metadataUrl: marketInfo["url"], + }); + this.bind("openBy", vipMarketView, "openBy"); + const page = this.addTab(marketInfo["label"], marketInfo["icon"], vipMarketView); + page.category = marketInfo["category"]; return page; }, + + openCategory: function(category) { + const viewFound = this.getChildControl("tabs-view").getChildren().find(view => view.category === category); + if (viewFound) { + this._openPage(viewFound); + return true; + } + return false; + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js b/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js index d01207f883f..c610abf36b3 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/MarketWindow.js @@ -18,10 +18,9 @@ qx.Class.define("osparc.vipMarket.MarketWindow", { extend: osparc.ui.window.TabbedWindow, - construct: function() { + construct: function(nodeId, category) { this.base(arguments, "store", this.tr("Market")); - osparc.utils.Utils.setIdToWidget(this, "storeWindow"); const width = 1035; @@ -29,26 +28,27 @@ qx.Class.define("osparc.vipMarket.MarketWindow", { this.set({ width, height - }) + }); - const vipMarket = this.__vipMarket = new osparc.vipMarket.Market(); + const vipMarket = this.__vipMarket = new osparc.vipMarket.Market().set({ + openBy: nodeId ? nodeId : null, + }); this._setTabbedView(vipMarket); + + if (category) { + vipMarket.openCategory(category); + } }, statics: { - openWindow: function() { - const storeWindow = new osparc.vipMarket.MarketWindow(); - storeWindow.center(); - storeWindow.open(); - return storeWindow; + openWindow: function(nodeId, category) { + if (osparc.product.Utils.showS4LStore()) { + const storeWindow = new osparc.vipMarket.MarketWindow(nodeId, category); + storeWindow.center(); + storeWindow.open(); + return storeWindow; + } + return null; } }, - - members: { - __vipMarket: null, - - openVipMarket: function() { - return this.__vipMarket.openVipMarket(); - }, - } }); diff --git a/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js b/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js index ff0af06af15..1385264933e 100644 --- a/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js +++ b/services/static-webserver/client/source/class/osparc/vipMarket/VipMarket.js @@ -21,11 +21,27 @@ qx.Class.define("osparc.vipMarket.VipMarket", { construct: function() { this.base(arguments); - this._setLayout(new qx.ui.layout.VBox(10)); + this._setLayout(new qx.ui.layout.HBox(10)); this.__buildLayout(); }, + properties: { + openBy: { + check: "String", + init: null, + nullable: true, + event: "changeOpenBy", + }, + + metadataUrl: { + check: "String", + init: null, + nullable: false, + apply: "__fetchModels", + } + }, + statics: { curateAnatomicalModels: function(anatomicalModelsRaw) { const anatomicalModels = []; @@ -46,9 +62,6 @@ qx.Class.define("osparc.vipMarket.VipMarket", { } else { curatedModel[key] = model[key]; } - if (key === "ID") { - curatedModel["leased"] = [22].includes(model[key]); - } }); anatomicalModels.push(curatedModel); }); @@ -57,53 +70,95 @@ qx.Class.define("osparc.vipMarket.VipMarket", { }, members: { - __anatomicalModelsModel: null, __anatomicalModels: null, - __sortByButton: null, - - __buildLayout: function() { - const toolbarLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)).set({ - alignY: "middle", - }); - this._add(toolbarLayout); - - const sortModelsButtons = this.__sortByButton = new osparc.vipMarket.SortModelsButtons().set({ - alignY: "bottom", - maxHeight: 27, - }); - toolbarLayout.add(sortModelsButtons); + __purchasesItems: null, + __anatomicalModelsModel: null, - const filter = new osparc.filter.TextFilter("text", "vipModels").set({ - alignY: "middle", - allowGrowY: false, - minWidth: 170, - }); - this.addListener("appear", () => filter.getChildControl("textfield").focus()); - toolbarLayout.add(filter); + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "left-side": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)).set({ + alignY: "middle", + }); + this._add(control); + break; + case "right-side": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)).set({ + alignY: "middle", + }); + this._add(control, { + flex: 1 + }); + break; + case "toolbar-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)).set({ + alignY: "middle", + }); + this.getChildControl("left-side").add(control); + break; + case "sort-button": + control = new osparc.vipMarket.SortModelsButtons().set({ + alignY: "bottom", + maxHeight: 27, + }); + this.getChildControl("toolbar-layout").add(control); + break; + case "filter-text": + control = new osparc.filter.TextFilter("text", "vipModels").set({ + alignY: "middle", + allowGrowY: false, + minWidth: 160, + }); + control.getChildControl("textfield").set({ + backgroundColor: "transparent", + }); + this.addListener("appear", () => control.getChildControl("textfield").focus()); + this.getChildControl("toolbar-layout").add(control, { + flex: 1 + }); + break; + case "models-list": + control = new qx.ui.form.List().set({ + decorator: "no-border", + spacing: 5, + minWidth: 250, + maxWidth: 250 + }); + this.getChildControl("left-side").add(control, { + flex: 1 + }); + break; + case "models-details": + control = new osparc.vipMarket.AnatomicalModelDetails().set({ + padding: 5, + }); + this.bind("openBy", control, "openBy"); + this.getChildControl("right-side").add(control, { + flex: 1 + }); + break; + } + return control || this.base(arguments, id); + }, - const modelsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); - this._add(modelsLayout, { - flex: 1 - }); - - const modelsUIList = new qx.ui.form.List().set({ - decorator: "no-border", - spacing: 5, - minWidth: 250, - maxWidth: 250 - }); - modelsLayout.add(modelsUIList) + __buildLayout: function() { + this.getChildControl("sort-button"); + this.getChildControl("filter-text"); + const modelsUIList = this.getChildControl("models-list"); const anatomicalModelsModel = this.__anatomicalModelsModel = new qx.data.Array(); const membersCtrl = new qx.data.controller.List(anatomicalModelsModel, modelsUIList, "name"); membersCtrl.setDelegate({ createItem: () => new osparc.vipMarket.AnatomicalModelListItem(), bindItem: (ctrl, item, id) => { - ctrl.bindProperty("id", "modelId", null, item, id); + ctrl.bindProperty("modelId", "modelId", null, item, id); ctrl.bindProperty("thumbnail", "thumbnail", null, item, id); ctrl.bindProperty("name", "name", null, item, id); ctrl.bindProperty("date", "date", null, item, id); - ctrl.bindProperty("leased", "leased", null, item, id); + ctrl.bindProperty("licensedItemId", "licensedItemId", null, item, id); + ctrl.bindProperty("pricingPlanId", "pricingPlanId", null, item, id); + ctrl.bindProperty("purchases", "purchases", null, item, id); }, configureItem: item => { item.subscribeToFilterGroup("vipModels"); @@ -117,18 +172,13 @@ qx.Class.define("osparc.vipMarket.VipMarket", { }; this.__anatomicalModelsModel.append(qx.data.marshal.Json.createModel(loadingModel)); - const anatomicModelDetails = new osparc.vipMarket.AnatomicalModelDetails().set({ - padding: 20, - }); - modelsLayout.add(anatomicModelDetails, { - flex: 1 - }); + const anatomicModelDetails = this.getChildControl("models-details"); modelsUIList.addListener("changeSelection", e => { const selection = e.getData(); if (selection.length) { const modelId = selection[0].getModelId(); - const modelFound = this.__anatomicalModels.find(anatomicalModel => anatomicalModel["ID"] === modelId); + const modelFound = this.__anatomicalModels.find(anatomicalModel => anatomicalModel["modelId"] === modelId); if (modelFound) { anatomicModelDetails.setAnatomicalModelsData(modelFound); return; @@ -136,47 +186,145 @@ qx.Class.define("osparc.vipMarket.VipMarket", { } anatomicModelDetails.setAnatomicalModelsData(null); }, this); + }, - fetch("https://itis.swiss/PD_DirectDownload/getDownloadableItems/AnatomicalModels", { + __fetchModels: function(url) { + fetch(url, { method:"POST" }) .then(resp => resp.json()) .then(anatomicalModelsRaw => { - this.__anatomicalModels = this.self().curateAnatomicalModels(anatomicalModelsRaw); - this.__populateModels(); - - anatomicModelDetails.addListener("modelLeased", e => { - const modelId = e.getData(); - const found = this.__anatomicalModels.find(model => model["ID"] === modelId); - if (found) { - found["leased"] = true; + const allAnatomicalModels = this.self().curateAnatomicalModels(anatomicalModelsRaw); + + const store = osparc.store.Store.getInstance(); + const contextWallet = store.getContextWallet(); + if (!contextWallet) { + return; + } + const walletId = contextWallet.getWalletId(); + const purchasesParams = { + url: { + walletId + } + }; + Promise.all([ + osparc.data.Resources.get("licensedItems"), + osparc.data.Resources.fetch("wallets", "purchases", purchasesParams), + ]) + .then(values => { + const licensedItems = values[0]; + const purchasesItems = values[1]; + this.__purchasesItems = purchasesItems; + + this.__anatomicalModels = []; + allAnatomicalModels.forEach(model => { + const modelId = model["ID"]; + const licensedItem = licensedItems.find(licItem => licItem["name"] == modelId); + if (licensedItem) { + const anatomicalModel = {}; + anatomicalModel["modelId"] = model["ID"]; + anatomicalModel["thumbnail"] = model["Thumbnail"]; + anatomicalModel["name"] = model["Features"]["name"] + " " + model["Features"]["version"]; + anatomicalModel["description"] = model["Description"]; + anatomicalModel["features"] = model["Features"]; + anatomicalModel["date"] = new Date(model["Features"]["date"]); + anatomicalModel["DOI"] = model["DOI"]; + // attach license data + anatomicalModel["licensedItemId"] = licensedItem["licensedItemId"]; + anatomicalModel["pricingPlanId"] = licensedItem["pricingPlanId"]; + // attach leased data + anatomicalModel["purchases"] = []; // default + const purchasesItemsFound = purchasesItems.filter(purchasesItem => purchasesItem["licensedItemId"] === licensedItem["licensedItemId"]); + if (purchasesItemsFound.length) { + purchasesItemsFound.forEach(purchasesItemFound => { + anatomicalModel["purchases"].push({ + expiresAt: new Date(purchasesItemFound["expireAt"]), + numberOfSeats: purchasesItemFound["numOfSeats"], + }) + }); + } + this.__anatomicalModels.push(anatomicalModel); + } + }); + this.__populateModels(); - anatomicModelDetails.setAnatomicalModelsData(found); - }; - }, this); + + const anatomicModelDetails = this.getChildControl("models-details"); + anatomicModelDetails.addListener("modelPurchaseRequested", e => { + if (!contextWallet) { + return; + } + const { + modelId, + licensedItemId, + pricingPlanId, + pricingUnitId, + } = e.getData(); + let numberOfSeats = null; + const pricingUnit = osparc.store.Pricing.getInstance().getPricingUnit(pricingPlanId, pricingUnitId); + if (pricingUnit) { + const split = pricingUnit.getName().split(" "); + numberOfSeats = parseInt(split[0]); + } + const params = { + url: { + licensedItemId + }, + data: { + "wallet_id": walletId, + "pricing_plan_id": pricingPlanId, + "pricing_unit_id": pricingUnitId, + "num_of_seats": numberOfSeats, // this should go away + }, + } + osparc.data.Resources.fetch("licensedItems", "purchase", params) + .then(() => { + const expirationDate = new Date(); + expirationDate.setMonth(expirationDate.getMonth() + 1); // rented for one month + const purchaseData = { + expiresAt: expirationDate, // get this info from the response + numberOfSeats, // get this info from the response + }; + + let msg = numberOfSeats; + msg += " seat" + (purchaseData["numberOfSeats"] > 1 ? "s" : ""); + msg += " rented until " + osparc.utils.Utils.formatDate(purchaseData["expiresAt"]); + osparc.FlashMessenger.getInstance().logAs(msg, "INFO"); + + const found = this.__anatomicalModels.find(model => model["modelId"] === modelId); + if (found) { + found["purchases"].push(purchaseData); + this.__populateModels(); + anatomicModelDetails.setAnatomicalModelsData(found); + } + }) + .catch(err => { + const msg = err.message || this.tr("Cannot purchase model"); + osparc.FlashMessenger.getInstance().logAs(msg, "ERROR"); + }); + }, this); + + anatomicModelDetails.addListener("modelImportRequested", e => { + const { + modelId + } = e.getData(); + this.__sendImportModelMessage(modelId); + }, this); + }); }) .catch(err => console.error(err)); }, __populateModels: function() { - const models = []; - this.__anatomicalModels.forEach(model => { - const anatomicalModel = {}; - anatomicalModel["id"] = model["ID"]; - anatomicalModel["thumbnail"] = model["Thumbnail"]; - anatomicalModel["name"] = model["Features"]["name"] + " " + model["Features"]["version"]; - anatomicalModel["date"] = new Date(model["Features"]["date"]); - anatomicalModel["leased"] = model["leased"]; - models.push(anatomicalModel); - }); + const models = this.__anatomicalModels; this.__anatomicalModelsModel.removeAll(); const sortModel = sortBy => { models.sort((a, b) => { // first criteria - if (b["leased"] !== a["leased"]) { + if (b["purchases"].length !== a["purchases"].length) { // leased first - return b["leased"] - a["leased"]; + return b["purchases"].length - a["purchases"].length; } // second criteria if (sortBy) { @@ -184,16 +332,14 @@ qx.Class.define("osparc.vipMarket.VipMarket", { if (sortBy["order"] === "down") { // A -> Z return a["name"].localeCompare(b["name"]); - } else { - return b["name"].localeCompare(a["name"]); } + return b["name"].localeCompare(a["name"]); } else if (sortBy["sort"] === "date") { if (sortBy["order"] === "down") { // Now -> Yesterday return b["date"] - a["date"]; - } else { - return a["date"] - b["date"]; } + return a["date"] - b["date"]; } } // default criteria @@ -204,12 +350,33 @@ qx.Class.define("osparc.vipMarket.VipMarket", { sortModel(); models.forEach(model => this.__anatomicalModelsModel.append(qx.data.marshal.Json.createModel(model))); - this.__sortByButton.addListener("sortBy", e => { + this.getChildControl("sort-button").addListener("sortBy", e => { this.__anatomicalModelsModel.removeAll(); const sortBy = e.getData(); sortModel(sortBy); models.forEach(model => this.__anatomicalModelsModel.append(qx.data.marshal.Json.createModel(model))); }, this); }, + + __sendImportModelMessage: function(modelId) { + const nodeId = this.getOpenBy(); + if (nodeId) { + const store = osparc.store.Store.getInstance(); + const currentStudy = store.getCurrentStudy(); + if (!currentStudy) { + return; + } + const node = currentStudy.getWorkbench().getNode(nodeId); + if (node && node.getIFrame()) { + const msg = { + "type": "importModel", + "message": { + "modelId": modelId, + }, + }; + node.getIFrame().sendMessageToIframe(msg); + } + } + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js b/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js index 83b128e82c9..f5f48cf30d8 100644 --- a/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js +++ b/services/static-webserver/client/source/class/osparc/widget/PersistentIframe.js @@ -252,19 +252,8 @@ qx.Class.define("osparc.widget.PersistentIframe", { __attachTriggerers: function() { this.postThemeSwitch = theme => { - const iframe = this._getIframeElement(); - if (iframe) { - const iframeDomEl = iframe.getDomElement(); - const iframeSource = iframe.getSource(); - if (iframeDomEl && iframeSource) { - const msg = "osparc;theme=" + theme; - try { - iframeDomEl.contentWindow.postMessage(msg, iframeSource); - } catch (err) { - console.log(`Failed posting message ${msg} to iframe ${iframeSource}\n${err.message}`); - } - } - } + const msg = "osparc;theme=" + theme; + this.sendMessageToIframe(msg); }; this.themeSwitchHandler = msg => { @@ -273,6 +262,21 @@ qx.Class.define("osparc.widget.PersistentIframe", { qx.event.message.Bus.getInstance().subscribe("themeSwitch", this.themeSwitchHandler); }, + sendMessageToIframe: function(msg) { + const iframe = this._getIframeElement(); + if (iframe) { + const iframeDomEl = iframe.getDomElement(); + const iframeSource = iframe.getSource(); + if (iframeDomEl && iframeSource) { + try { + iframeDomEl.contentWindow.postMessage(msg, iframeSource); + } catch (err) { + console.log(`Failed posting message ${msg} to iframe ${iframeSource}\n${err.message}`); + } + } + } + }, + __attachListeners: function() { this.__iframe.addListener("load", () => { const iframe = this._getIframeElement(); @@ -282,7 +286,9 @@ qx.Class.define("osparc.widget.PersistentIframe", { window.addEventListener("message", message => { const data = message.data; if (data) { - this.__handleIframeMessage(data); + const origin = new URL(message.origin).hostname; // nodeId.services.deployment + const nodeId = origin.split(".")[0]; + this.__handleIframeMessage(data, nodeId); } }); } @@ -290,19 +296,27 @@ qx.Class.define("osparc.widget.PersistentIframe", { }, this); }, - __handleIframeMessage: function(data) { + __handleIframeMessage: function(data, nodeId) { if (data["type"] && data["message"]) { - if (data["type"] === "theme") { - // switch theme driven by the iframe - const message = data["message"]; - if (message.includes("osparc;theme=")) { - const themeName = message.replace("osparc;theme=", ""); - const validThemes = osparc.ui.switch.ThemeSwitcher.getValidThemes(); - const themeFound = validThemes.find(theme => theme.basename === themeName); - const themeManager = qx.theme.manager.Meta.getInstance(); - if (themeFound !== themeManager.getTheme()) { - themeManager.setTheme(themeFound); + switch (data["type"]) { + case "theme": { + // switch theme driven by the iframe + const message = data["message"]; + if (message.includes("osparc;theme=")) { + const themeName = message.replace("osparc;theme=", ""); + const validThemes = osparc.ui.switch.ThemeSwitcher.getValidThemes(); + const themeFound = validThemes.find(theme => theme.basename === themeName); + const themeManager = qx.theme.manager.Meta.getInstance(); + if (themeFound !== themeManager.getTheme()) { + themeManager.setTheme(themeFound); + } } + break; + } + case "openMarket": { + const category = data["message"] && data["message"]["category"]; + osparc.vipMarket.MarketWindow.openWindow(nodeId, category); + break; } } }