diff --git a/services/static-webserver/client/source/class/osparc/component/resourceUsage/Overview.js b/services/static-webserver/client/source/class/osparc/component/resourceUsage/Overview.js new file mode 100644 index 00000000000..af4060a9adc --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/component/resourceUsage/Overview.js @@ -0,0 +1,181 @@ +/* ************************************************************************ + + 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.component.resourceUsage.Overview", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(15)); + + const loadingImage = this.getChildControl("loading-image"); + loadingImage.show(); + const table = this.getChildControl("usage-table"); + table.exclude(); + + this.__fetchData(); + }, + + statics: { + ITEMS_PER_PAGE: 15, + + popUpInWindow: function() { + const title = qx.locale.Manager.tr("Usage Overview"); + const noteEditor = new osparc.component.resourceUsage.Overview(); + const viewWidth = 900; + const viewHeight = 450; + const win = osparc.ui.window.Window.popUpInWindow(noteEditor, title, viewWidth, viewHeight); + win.center(); + win.open(); + return win; + } + }, + + members: { + __prevRequestParams: null, + __nextRequestParams: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "loading-image": + control = new qx.ui.basic.Image().set({ + source: "@FontAwesome5Solid/circle-notch/64", + alignX: "center", + alignY: "middle" + }); + control.getContentElement().addClass("rotate"); + this._add(control); + break; + case "usage-table": + control = new osparc.component.resourceUsage.OverviewTable().set({ + height: (this.self().ITEMS_PER_PAGE*20 + 40) + }); + this._add(control); + break; + case "page-buttons": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ + allowGrowX: true, + alignX: "center", + alignY: "middle" + }); + this._add(control); + break; + case "prev-page-button": { + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/chevron-left/12", + allowGrowX: false + }); + control.addListener("execute", () => this.__fetchData(this.__getPrevRequest())); + const pageButtons = this.getChildControl("page-buttons"); + pageButtons.add(control); + break; + } + case "current-page-label": { + control = new qx.ui.basic.Label().set({ + font: "text-14", + textAlign: "center", + alignY: "middle" + }); + const pageButtons = this.getChildControl("page-buttons"); + pageButtons.add(control); + break; + } + case "next-page-button": { + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/chevron-right/12", + allowGrowX: false + }); + control.addListener("execute", () => this.__fetchData(this.__getNextRequest())); + const pageButtons = this.getChildControl("page-buttons"); + pageButtons.add(control); + break; + } + } + return control || this.base(arguments, id); + }, + + __fetchData: function(request) { + const loadingImage = this.getChildControl("loading-image"); + loadingImage.show(); + const table = this.getChildControl("usage-table"); + table.exclude(); + + if (request === undefined) { + request = this.__getNextRequest(); + } + request + .then(resp => { + const data = resp["data"]; + this.__setData(data); + this.__prevRequestParams = resp["_links"]["prev"]; + this.__nextRequestParams = resp["_links"]["next"]; + this.__evaluatePageButtons(resp); + }) + .finally(() => { + loadingImage.exclude(); + table.show(); + }); + }, + + __getPrevRequest: function() { + const params = { + url: { + offset: osparc.component.resourceUsage.Overview.ITEMS_PER_PAGE, + limit: osparc.component.resourceUsage.Overview.ITEMS_PER_PAGE + } + }; + if (this.__prevRequestParams) { + params.url.offset = osparc.utils.Utils.getParamFromURL(this.__prevRequestParams, "offset"); + params.url.limit = osparc.utils.Utils.getParamFromURL(this.__prevRequestParams, "limit"); + } + const options = { + resolveWResponse: true + }; + return osparc.data.Resources.fetch("resourceUsage", "getPage", params, undefined, options); + }, + + __getNextRequest: function() { + const params = { + url: { + offset: 0, + limit: osparc.component.resourceUsage.Overview.ITEMS_PER_PAGE + } + }; + if (this.__nextRequestParams) { + params.url.offset = osparc.utils.Utils.getParamFromURL(this.__nextRequestParams, "offset"); + params.url.limit = osparc.utils.Utils.getParamFromURL(this.__nextRequestParams, "limit"); + } + const options = { + resolveWResponse: true + }; + return osparc.data.Resources.fetch("resourceUsage", "getPage", params, undefined, options); + }, + + __setData: function(data) { + const table = this.getChildControl("usage-table"); + table.addData(data); + }, + + __evaluatePageButtons:function(resp) { + this.getChildControl("prev-page-button").setEnabled(Boolean(this.__prevRequestParams)); + this.getChildControl("current-page-label").setValue(((resp["_meta"]["offset"]/this.self().ITEMS_PER_PAGE)+1).toString()); + this.getChildControl("next-page-button").setEnabled(Boolean(this.__nextRequestParams)); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/component/resourceUsage/OverviewTable.js b/services/static-webserver/client/source/class/osparc/component/resourceUsage/OverviewTable.js new file mode 100644 index 00000000000..39f5604364a --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/component/resourceUsage/OverviewTable.js @@ -0,0 +1,105 @@ +/* ************************************************************************ + + 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.component.resourceUsage.OverviewTable", { + extend: osparc.ui.table.Table, + + construct: function() { + const cols = this.self().COLUMNS; + const model = this.__model = new qx.ui.table.model.Simple(); + const colNames = Object.values(cols).map(col => col.title); + model.setColumns(colNames); + + this.base(arguments, model, { + tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj), + statusBarVisible: false + }); + const columnModel = this.getTableColumnModel(); + columnModel.getBehavior().setWidth(this.self().COLUMNS.duration.pos, 60); + columnModel.getBehavior().setWidth(this.self().COLUMNS.processors.pos, 80); + columnModel.getBehavior().setWidth(this.self().COLUMNS.coreHours.pos, 80); + columnModel.getBehavior().setWidth(this.self().COLUMNS.status.pos, 70); + columnModel.setDataCellRenderer(this.self().COLUMNS.duration.pos, new qx.ui.table.cellrenderer.Number()); + columnModel.setDataCellRenderer(this.self().COLUMNS.processors.pos, new qx.ui.table.cellrenderer.Number()); + columnModel.setDataCellRenderer(this.self().COLUMNS.coreHours.pos, new qx.ui.table.cellrenderer.Number()); + }, + + statics: { + COLUMNS: { + project: { + pos: 0, + title: "Project" + }, + node: { + pos: 1, + title: "Node" + }, + service: { + pos: 2, + title: "Service" + }, + start: { + pos: 3, + title: "Start" + }, + duration: { + pos: 4, + title: "Duration" + }, + processors: { + pos: 5, + title: "Processors" + }, + coreHours: { + pos: 6, + title: "Core Hours" + }, + status: { + pos: 7, + title: "Status" + } + } + }, + + members: { + __model: null, + + addData: function(datas) { + const newDatas = []; + if (datas) { + const cols = this.self().COLUMNS; + datas.forEach(data => { + const newData = []; + newData[cols["project"].pos] = data["project_name"] ? data["project_name"] : data["project_uuid"]; + newData[cols["node"].pos] = data["node_label"] ? data["node_label"] : data["node_uuid"]; + if (data["service_key"]) { + const parts = data["service_key"].split("/"); + const serviceName = parts.pop(); + newData[cols["service"].pos] = serviceName + ":" + data["service_version"]; + } + newData[cols["start"].pos] = osparc.utils.Utils.formatDateAndTime(new Date(data["start_time"])); + newData[cols["duration"].pos] = data["duration"]; + newData[cols["processors"].pos] = data["processors"]; + newData[cols["coreHours"].pos] = data["core_hours"]; + newData[cols["status"].pos] = qx.lang.String.firstUp(data["status"]); + newDatas.push(newData); + }); + } + this.setData(newDatas); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js index 5edcaed5f94..60d9ced3dc1 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js @@ -41,7 +41,7 @@ qx.Class.define("osparc.dashboard.ResourceMoreOptions", { statics: { WIDTH: 715, - HEIGHT: 715, + HEIGHT: 720, popUpInWindow: function(moreOpts) { const title = qx.locale.Manager.tr("Details"); diff --git a/services/static-webserver/client/source/class/osparc/data/Permissions.js b/services/static-webserver/client/source/class/osparc/data/Permissions.js index f3bd9dad99b..2cc94c58f3a 100644 --- a/services/static-webserver/client/source/class/osparc/data/Permissions.js +++ b/services/static-webserver/client/source/class/osparc/data/Permissions.js @@ -132,7 +132,8 @@ qx.Class.define("osparc.data.Permissions", { "study.nodestree.uuid.read", "study.filestree.uuid.read", "study.logger.debug.read", - "statics.read" + "statics.read", + "usage.all.read" ], "admin": [] }; 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 f3fe571aff4..ab795a4abc8 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -209,6 +209,15 @@ qx.Class.define("osparc.data.Resources", { } } }, + "resourceUsage": { + useCache: true, + endpoints: { + getPage: { + method: "GET", + url: statics.API + "/resource-usage/containers?offset={offset}&limit={limit}" + } + } + }, /* * NODES */ diff --git a/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js b/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js index 67d7cee7ade..b5a9fb58ef5 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js +++ b/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js @@ -99,6 +99,11 @@ qx.Class.define("osparc.navigation.UserMenuButton", { control.addListener("execute", () => osparc.desktop.organizations.OrganizationsWindow.openWindow(), this); this.getMenu().add(control); break; + case "usage-overview": + control = new qx.ui.menu.Button(this.tr("Usage Overview")); + control.addListener("execute", () => osparc.component.resourceUsage.Overview.popUpInWindow(), this); + this.getMenu().add(control); + break; case "clusters": control = new qx.ui.menu.Button(this.tr("Clusters")); control.exclude(); @@ -156,6 +161,9 @@ qx.Class.define("osparc.navigation.UserMenuButton", { } else { this.getChildControl("preferences"); this.getChildControl("organizations"); + if (osparc.data.Permissions.getInstance().canDo("usage.all.read")) { + this.getChildControl("usage-overview"); + } this.getChildControl("clusters"); } if (osparc.product.tutorial.Utils.getTutorial()) { @@ -189,6 +197,9 @@ qx.Class.define("osparc.navigation.UserMenuButton", { } else { this.getChildControl("preferences"); this.getChildControl("organizations"); + if (osparc.data.Permissions.getInstance().canDo("usage.all.read")) { + this.getChildControl("usage-overview"); + } this.getChildControl("clusters"); } this.getMenu().addSeparator(); 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 39469096c5a..22acd8837c8 100644 --- a/services/static-webserver/client/source/class/osparc/store/Store.js +++ b/services/static-webserver/client/source/class/osparc/store/Store.js @@ -70,6 +70,10 @@ qx.Class.define("osparc.store.Store", { check: "Array", init: [] }, + resourceUsage: { + check: "Array", + init: [] + }, nodesInStudyResources: { check: "Array", init: [] diff --git a/services/static-webserver/client/source/class/osparc/utils/Utils.js b/services/static-webserver/client/source/class/osparc/utils/Utils.js index 7f5c6b4744a..6272755b42c 100644 --- a/services/static-webserver/client/source/class/osparc/utils/Utils.js +++ b/services/static-webserver/client/source/class/osparc/utils/Utils.js @@ -712,9 +712,10 @@ qx.Class.define("osparc.utils.Utils", { return parsedFragment; }, - getParamFromURL: (url, param) => { - const urlParams = new URLSearchParams(url); - return urlParams.get(param); + getParamFromURL: (urlStr, param) => { + const url = new URL(urlStr); + const args = new URLSearchParams(url.search); + return args.get(param); }, hasParamFromURL: (url, param) => {