Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [Frontend] ViP Rentals and ViP Checkouts in Billing Center #7043

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5488979
Rentals in Billing Center
odeimaiz Jan 14, 2025
038f411
Show pruchases
odeimaiz Jan 14, 2025
110f767
sorting
odeimaiz Jan 14, 2025
7d20373
minor
odeimaiz Jan 14, 2025
dd52466
show name
odeimaiz Jan 14, 2025
d06227c
Transactions -> Payments
odeimaiz Jan 14, 2025
17d4f20
minor
odeimaiz Jan 14, 2025
af8603b
Merge branch 'master' into feature/rentals-table
odeimaiz Jan 15, 2025
3901cf6
Merge branch 'feature/rentals-table' of github.com:odeimaiz/osparc-si…
odeimaiz Jan 15, 2025
87512a2
LicensedItems store
odeimaiz Jan 15, 2025
8e0f469
fetchVipModels from Store
odeimaiz Jan 15, 2025
4b3d523
getLicensedItems
odeimaiz Jan 15, 2025
6235e08
LicensedItems cached
odeimaiz Jan 15, 2025
f27ad07
[skip ci] more to Store
odeimaiz Jan 15, 2025
82cd24d
more to Store
odeimaiz Jan 15, 2025
ecb8d22
[skip ci] purchases with pagination
odeimaiz Jan 15, 2025
559ed08
minor
odeimaiz Jan 15, 2025
6331c54
[skip ci] flat()
odeimaiz Jan 15, 2025
d30baaf
moved
odeimaiz Jan 15, 2025
a878808
Show model's name
odeimaiz Jan 15, 2025
4cc33cb
minor
odeimaiz Jan 15, 2025
9be4b92
[skip ci] minor
odeimaiz Jan 15, 2025
3a15b39
INFO
odeimaiz Jan 15, 2025
1d555ed
Checkouts
odeimaiz Jan 15, 2025
811e560
minors
odeimaiz Jan 15, 2025
a2e6ffc
fix checkouts sorting
odeimaiz Jan 15, 2025
edc686a
one more check
odeimaiz Jan 15, 2025
844196b
duration
odeimaiz Jan 15, 2025
4ee83b8
minor fixes
odeimaiz Jan 15, 2025
6584eaa
undo
odeimaiz Jan 15, 2025
17054d4
fix backend
odeimaiz Jan 15, 2025
19f6a67
minor
odeimaiz Jan 15, 2025
bdfb4c8
refactoring
odeimaiz Jan 15, 2025
d8616dd
minors
odeimaiz Jan 15, 2025
c097033
refactor
odeimaiz Jan 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -936,10 +936,6 @@ qx.Class.define("osparc.data.Resources", {
method: "PUT",
url: statics.API + "/wallets/{walletId}/auto-recharge"
},
purchases: {
method: "GET",
url: statics.API + "/wallets/{walletId}/licensed-items-purchases"
},
}
},
/*
Expand Down Expand Up @@ -1300,10 +1296,18 @@ qx.Class.define("osparc.data.Resources", {
method: "GET",
url: statics.API + "/catalog/licensed-items?offset={offset}&limit={limit}"
},
purchases: {
method: "GET",
url: statics.API + "/wallets/{walletId}/licensed-items-purchases?offset={offset}&limit={limit}"
},
purchase: {
method: "POST",
url: statics.API + "/catalog/licensed-items/{licensedItemId}:purchase"
},
checkouts: {
method: "GET",
url: statics.API + "/wallets/{walletId}/licensed-items-checkouts?offset={offset}&limit={limit}"
},
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", {
if (osparc.data.Permissions.getInstance().canDo("usage.all.read")) {
this.__usagePage = this.__addUsagePage();
}

if (osparc.product.Utils.showS4LStore()) {
this.__addPurchasesPage();
this.__addCheckoutsPage();
}
},

statics: {
Expand Down Expand Up @@ -80,7 +85,7 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", {
},

__addTransactionsPage: function() {
const title = this.tr("Transactions");
const title = this.tr("Payments");
const iconSrc = "@FontAwesome5Solid/exchange-alt/22";
const transactions = this.__transactionsTable = new osparc.desktop.credits.Transactions();
const page = this.addTab(title, iconSrc, transactions);
Expand All @@ -95,6 +100,22 @@ qx.Class.define("osparc.desktop.credits.BillingCenter", {
return page;
},

__addPurchasesPage: function() {
const title = this.tr("Purchases");
const iconSrc = "@FontAwesome5Solid/list/22";
const purchases = new osparc.desktop.credits.Purchases();
const page = this.addTab(title, iconSrc, purchases);
return page;
},

__addCheckoutsPage: function() {
const title = this.tr("Checkouts");
const iconSrc = "@FontAwesome5Solid/list/22";
const purchases = new osparc.desktop.credits.Checkouts();
const page = this.addTab(title, iconSrc, purchases);
return page;
},

openWallets: function() {
if (this.__walletsPage) {
return this._openPage(this.__walletsPage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* ************************************************************************

osparc - the simcore frontend

https://osparc.io

Copyright:
2025 IT'IS Foundation, https://itis.swiss

License:
MIT: https://opensource.org/licenses/MIT

Authors:
* Odei Maiz (odeimaiz)

************************************************************************ */


qx.Class.define("osparc.desktop.credits.Checkouts", {
extend: osparc.desktop.credits.ResourceInTableViewer,

members: {
_createChildControlImpl: function(id) {
let control;
switch (id) {
case "table": {
const dateFilters = this.getChildControl("date-filters");
control = new osparc.desktop.credits.CheckoutsTable(this._getSelectWalletId(), dateFilters.getValue()).set({
marginTop: 10
});
const fetchingImage = this.getChildControl("fetching-image");
control.getTableModel().bind("isFetching", fetchingImage, "visibility", {
converter: isFetching => isFetching ? "visible" : "excluded"
})
this._add(control, { flex: 1 })
break;
}
}
return control || this.base(arguments, id);
},

_buildLayout: function() {
this.base(arguments);

this.getChildControl("export-button").exclude();
},
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* ************************************************************************
osparc - the simcore frontend
https://osparc.io
Copyright:
2025 IT'IS Foundation, https://itis.swiss
License:
MIT: https://opensource.org/licenses/MIT
Authors:
* Odei Maiz (odeimaiz)
************************************************************************ */


qx.Class.define("osparc.desktop.credits.CheckoutsTable", {
extend: qx.ui.table.Table,

construct: function(walletId, filters) {
this.base(arguments);

const model = new osparc.desktop.credits.CheckoutsTableModel(walletId, filters);
this.setTableModel(model);

this.set({
statusBarVisible: false,
headerCellHeight: 26,
rowHeight: 26,
});

const columnModel = this.getTableColumnModel();
columnModel.setColumnVisible(this.self().COLS.CHECKOUT_ID.column, false);
columnModel.setColumnVisible(this.self().COLS.ITEM_ID.column, false);

Object.values(this.self().COLS).forEach(col => columnModel.setColumnWidth(col.column, col.width));
},

statics: {
COLS: {
CHECKOUT_ID: {
id: "checkoutId",
column: 0,
label: qx.locale.Manager.tr("CheckoutId"),
width: 150
},
ITEM_ID: {
id: "itemId",
column: 1,
label: qx.locale.Manager.tr("ItemId"),
width: 150
},
ITEM_LABEL: {
id: "itemLabel",
column: 2,
label: qx.locale.Manager.tr("Name"),
width: 150
},
START: {
id: "start",
column: 3,
label: qx.locale.Manager.tr("Start"),
width: 150
},
DURATION: {
id: "duration",
column: 4,
label: qx.locale.Manager.tr("Duration"),
width: 150
},
SEATS: {
id: "seats",
column: 5,
label: qx.locale.Manager.tr("Seats"),
width: 50
},
USER: {
id: "user",
column: 6,
label: qx.locale.Manager.tr("User"),
width: 100
},
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
/* ************************************************************************

osparc - the simcore frontend

https://osparc.io

Copyright:
2025 IT'IS Foundation, https://itis.swiss

License:
MIT: https://opensource.org/licenses/MIT

Authors:
* Odei Maiz (odeimaiz)

************************************************************************ */


qx.Class.define("osparc.desktop.credits.CheckoutsTableModel", {
extend: qx.ui.table.model.Remote,

construct(walletId, filters) {
this.base(arguments);

const checkoutsCols = osparc.desktop.credits.CheckoutsTable.COLS;
const colLabels = Object.values(checkoutsCols).map(col => col.label);
const colIDs = Object.values(checkoutsCols).map(col => col.id);

this.setColumns(colLabels, colIDs);
this.setWalletId(walletId);
if (filters) {
this.setFilters(filters);
}
this.setSortColumnIndexWithoutSortingData(checkoutsCols.START.column);
this.setSortAscendingWithoutSortingData(false);
this.setColumnSortable(checkoutsCols.DURATION.column, false);
},

properties: {
walletId: {
check: "Number",
nullable: true
},

filters: {
check: "Object",
init: null
},

isFetching: {
check: "Boolean",
init: false,
event: "changeFetching"
},

orderBy: {
check: "Object",
init: {
field: "startAt",
direction: "desc"
}
},
},

statics: {
SERVER_MAX_LIMIT: 49,
COLUMN_ID_TO_DB_COLUMN_MAP: {
0: "startAt",
},
},

members: {
// overridden
sortByColumn(columnIndex, ascending) {
this.setOrderBy({
field: this.self().COLUMN_ID_TO_DB_COLUMN_MAP[columnIndex],
direction: ascending ? "asc" : "desc"
})
this.base(arguments, columnIndex, ascending);
},

// overridden
_loadRowCount() {
const walletId = this.getWalletId();
const urlParams = {
offset: 0,
limit: 1,
filters: this.getFilters() ?
JSON.stringify({
"startAt": this.getFilters()
}) :
null,
orderBy: JSON.stringify(this.getOrderBy()),
};
const options = {
resolveWResponse: true
};
osparc.store.LicensedItems.getInstance().getCheckedOutLicensedItems(walletId, urlParams, options)
.then(resp => this._onRowCountLoaded(resp["_meta"].total))
.catch(() => this._onRowCountLoaded(null));
},

// overridden
_loadRowData(firstRow, qxLastRow) {
this.setIsFetching(true);

const lastRow = Math.min(qxLastRow, this._rowCount - 1);
// Returns a request promise with given offset and limit
const getFetchPromise = (offset, limit=this.self().SERVER_MAX_LIMIT) => {
const walletId = this.getWalletId();
const urlParams = {
limit,
offset,
filters: this.getFilters() ?
JSON.stringify({
"started_at": this.getFilters()
}) :
null,
orderBy: JSON.stringify(this.getOrderBy())
};
const licensedItemsStore = osparc.store.LicensedItems.getInstance();
return Promise.all([
licensedItemsStore.getLicensedItems(),
licensedItemsStore.getCheckedOutLicensedItems(walletId, urlParams),
licensedItemsStore.getVipModels(),
])
.then(values => {
const licensedItems = values[0];
const checkoutsItems = values[1];
const vipModels = values[2];

const data = [];
const checkoutsCols = osparc.desktop.credits.CheckoutsTable.COLS;
checkoutsItems.forEach(checkoutsItem => {
const licensedItemId = checkoutsItem["licensedItemId"];
const licensedItem = licensedItems.find(licItem => licItem["licensedItemId"] === licensedItemId);
const vipModel = vipModels.find(vipMdl => licensedItem && (vipMdl["modelId"] == licensedItem["name"]));
let start = "";
let duration = "";
if (checkoutsItem["startedAt"]) {
start = osparc.utils.Utils.formatDateAndTime(new Date(checkoutsItem["startedAt"]));
if (checkoutsItem["stoppedAt"]) {
duration = osparc.utils.Utils.formatMsToHHMMSS(new Date(checkoutsItem["stoppedAt"]) - new Date(checkoutsItem["startedAt"]));
}
}
data.push({
[checkoutsCols.CHECKOUT_ID.id]: checkoutsItem["licensedItemCheckoutId"],
[checkoutsCols.ITEM_ID.id]: licensedItemId,
[checkoutsCols.ITEM_LABEL.id]: vipModel ? vipModel["name"] : "unknown model",
[checkoutsCols.START.id]: start,
[checkoutsCols.DURATION.id]: duration,
[checkoutsCols.SEATS.id]: checkoutsItem["numOfSeats"],
[checkoutsCols.USER.id]: checkoutsItem["userId"],
});
});
return data;
});
};

// Divides the model row request into several server requests to comply with the number of rows server limit
const reqLimit = lastRow - firstRow + 1; // Number of requested rows
const nRequests = Math.ceil(reqLimit / this.self().SERVER_MAX_LIMIT);
if (nRequests > 1) {
const requests = [];
for (let i=firstRow; i <= lastRow; i += this.self().SERVER_MAX_LIMIT) {
requests.push(getFetchPromise(i, i > lastRow - this.self().SERVER_MAX_LIMIT + 1 ? reqLimit % this.self().SERVER_MAX_LIMIT : this.self().SERVER_MAX_LIMIT))
}
Promise.all(requests)
.then(responses => this._onRowDataLoaded(responses.flat()))
.catch(err => {
console.error(err);
this._onRowDataLoaded(null);
})
.finally(() => this.setIsFetching(false));
} else {
getFetchPromise(firstRow, reqLimit)
.then(data => this._onRowDataLoaded(data))
.catch(err => {
console.error(err)
this._onRowDataLoaded(null);
})
.finally(() => this.setIsFetching(false));
}
}
}
})
Loading
Loading