diff --git a/services/web/client/source/class/osparc/component/export/Permissions.js b/services/web/client/source/class/osparc/component/export/Permissions.js index 90bda71e574..4d8f71b9733 100644 --- a/services/web/client/source/class/osparc/component/export/Permissions.js +++ b/services/web/client/source/class/osparc/component/export/Permissions.js @@ -33,7 +33,7 @@ qx.Class.define("osparc.component.export.Permissions", { construct: function(studyData) { this.base(arguments); - this.__studyData = osparc.utils.Utils.deepCloneObject(studyData); + this.__studyData = osparc.data.model.Study.deepCloneStudyObject(studyData); this._setLayout(new qx.ui.layout.VBox(15)); diff --git a/services/web/client/source/class/osparc/component/export/SaveAsTemplate.js b/services/web/client/source/class/osparc/component/export/SaveAsTemplate.js index eca7b4a5728..caba14bedbe 100644 --- a/services/web/client/source/class/osparc/component/export/SaveAsTemplate.js +++ b/services/web/client/source/class/osparc/component/export/SaveAsTemplate.js @@ -34,7 +34,7 @@ qx.Class.define("osparc.component.export.SaveAsTemplate", { this._setLayout(new qx.ui.layout.VBox(5)); this.__studyId = studyId; - this.__formData = osparc.utils.Utils.deepCloneObject(studyData); + this.__formData = osparc.data.model.Study.deepCloneStudyObject(studyData); this.__buildLayout(); diff --git a/services/web/client/source/class/osparc/dashboard/ExploreBrowser.js b/services/web/client/source/class/osparc/dashboard/ExploreBrowser.js index 9936c124f3d..7d12f10d405 100644 --- a/services/web/client/source/class/osparc/dashboard/ExploreBrowser.js +++ b/services/web/client/source/class/osparc/dashboard/ExploreBrowser.js @@ -130,6 +130,8 @@ qx.Class.define("osparc.dashboard.ExploreBrowser", { __initResources: function() { this.__showLoadingPage(this.tr("Discovering Templates and Apps")); + this.__templateStudies = []; + this.__services = []; const servicesTags = this.__getTags(); const store = osparc.store.Store.getInstance(); const servicesPromise = store.getServicesDAGs(true); diff --git a/services/web/client/source/class/osparc/dashboard/StudyBrowser.js b/services/web/client/source/class/osparc/dashboard/StudyBrowser.js index 74ecb71ae59..ef30b74ec18 100644 --- a/services/web/client/source/class/osparc/dashboard/StudyBrowser.js +++ b/services/web/client/source/class/osparc/dashboard/StudyBrowser.js @@ -90,13 +90,8 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { } }, - __reloadUserStudy: function(studyId) { - const params = { - url: { - projectId: studyId - } - }; - osparc.data.Resources.getOne("studies", params) + __reloadUserStudy: function(studyId, reload) { + osparc.store.Store.getInstance().getStudyWState(studyId, reload) .then(studyData => { this.__resetStudyItem(studyData); }) @@ -111,7 +106,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { */ reloadUserStudies: function() { if (osparc.data.Permissions.getInstance().canDo("studies.user.read")) { - osparc.data.Resources.get("studies") + osparc.store.Store.getInstance().getStudiesWState() .then(studies => { this.__resetStudyList(studies); this.resetSelection(); @@ -127,14 +122,15 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { __initResources: function() { this.__showLoadingPage(this.tr("Loading Studies")); - const servicesTags = this.__getTags(); + this.__userStudies = []; + const resourcePromises = []; const store = osparc.store.Store.getInstance(); - const servicesPromise = store.getServicesDAGs(true); - - Promise.all([ - servicesTags, - servicesPromise - ]) + resourcePromises.push(store.getVisibleMembers()); + resourcePromises.push(store.getServicesDAGs(true)); + if (osparc.data.Permissions.getInstance().canDo("study.tag")) { + resourcePromises.push(osparc.data.Resources.get("tags")); + } + Promise.all(resourcePromises) .then(() => { this.__hideLoadingPage(); this.__createStudiesLayout(); @@ -144,7 +140,8 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { if (loadStudyId) { this.__getStudyAndStart(loadStudyId); } - }); + }) + .catch(console.error); }, __reloadResources: function() { @@ -171,18 +168,6 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }); }, - __getTags: function() { - return new Promise((resolve, reject) => { - if (osparc.data.Permissions.getInstance().canDo("study.tag")) { - osparc.data.Resources.get("tags") - .catch(console.error) - .finally(() => resolve()); - } else { - resolve(); - } - }); - }, - __createStudiesLayout: function() { const studyFilters = this.__studyFilters = new osparc.component.filter.group.StudyFilterGroup("studyBrowser").set({ paddingTop: 5 @@ -244,12 +229,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }, __getStudyAndStart: function(loadStudyId) { - const params = { - url: { - projectId: loadStudyId - } - }; - osparc.data.Resources.getOne("studies", params) + osparc.store.Store.getStudyWState(loadStudyId, true) .then(studyData => { this.__startStudy(studyData); }) @@ -282,6 +262,23 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { }, __attachEventHandlers: function() { + // Listen to socket + const socket = osparc.wrapper.WebSocket.getInstance(); + // callback for incoming logs + const slotName = "projectStateUpdated"; + socket.removeSlot(slotName); + socket.on(slotName, function(jsonString) { + const data = JSON.parse(jsonString); + if (data) { + const studyId = data["project_uuid"]; + const state = ("data" in data) ? data["data"] : {}; + const studyItem = this.__userStudyContainer.getChildren().find(card => (card instanceof osparc.dashboard.StudyBrowserButtonItem) && (card.getUuid() === studyId)); + if (studyItem) { + studyItem.setState(state); + } + } + }, this); + const textfield = this.__studyFilters.getTextFilter().getChildControl("textfield"); textfield.addListener("appear", () => { textfield.focus(); @@ -347,10 +344,12 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { __resetStudyItem: function(studyData) { const userStudyList = this.__userStudies; const index = userStudyList.findIndex(userStudy => userStudy["uuid"] === studyData["uuid"]); - if (index !== -1) { - this.__userStudies[index] = studyData; - this.__resetStudyList(userStudyList); + if (index === -1) { + userStudyList.push(studyData); + } else { + userStudyList[index] = studyData; } + this.__resetStudyList(userStudyList); }, __resetStudyList: function(userStudyList) { @@ -400,13 +399,16 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { accessRights: study.accessRights ? study.accessRights : null, lastChangeDate: study.lastChangeDate ? new Date(study.lastChangeDate) : null, icon: study.thumbnail || defaultThumbnail, + state: study.state ? study.state : {}, tags }); const menu = this.__getStudyItemMenu(item, study); item.setMenu(menu); item.subscribeToFilterGroup("studyBrowser"); item.addListener("execute", () => { - this.__itemClicked(item); + if (!item.isLocked()) { + this.__itemClicked(item); + } }, this); return item; @@ -469,7 +471,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { const permissionsView = new osparc.component.export.Permissions(studyData); permissionsView.addListener("updateStudy", e => { const studyId = e.getData(); - this.__reloadUserStudy(studyId); + this.__reloadUserStudy(studyId, true); }, this); const window = permissionsView.createWindow(); permissionsView.addListener("finished", e => { diff --git a/services/web/client/source/class/osparc/dashboard/StudyBrowserButtonItem.js b/services/web/client/source/class/osparc/dashboard/StudyBrowserButtonItem.js index b55950ad807..1901cabee08 100644 --- a/services/web/client/source/class/osparc/dashboard/StudyBrowserButtonItem.js +++ b/services/web/client/source/class/osparc/dashboard/StudyBrowserButtonItem.js @@ -38,15 +38,7 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { qx.locale.Date.getTimeFormat("short") ); - this.addListener("changeValue", e => { - const val = this.getValue(); - - const tick = this.getChildControl("tick-selected"); - tick.setVisibility(val ? "visible" : "excluded"); - - const untick = this.getChildControl("tick-unselected"); - untick.setVisibility(val ? "excluded" : "visible"); - }); + this.addListener("changeValue", this.__itemSelected, this); }, properties: { @@ -101,11 +93,29 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { tags: { check: "Array", apply: "_applyTags" + }, + + state: { + check: "Object", + nullable: false, + apply: "_applyState" + }, + + locked: { + check: "Boolean", + init: false, + nullable: false, + apply: "_applyLocked" + }, + + lockedBy: { + check: "String", + nullable: true, + apply: "_applyLockedBy" } }, statics: { - MENU_BTN_Z: 20, MENU_BTN_WIDTH: 25, SHARED_USER: "@FontAwesome5Solid/user/14", SHARED_ORGS: "@FontAwesome5Solid/users/14", @@ -121,8 +131,40 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { }, multiSelection: function(on) { + if (on) { + const menuButton = this.getChildControl("menu-button"); + menuButton.setVisibility("excluded"); + this.__itemSelected(); + } else { + this.__showMenuOnly(); + } + }, + + __itemSelected: function() { + if (this.isResourceType("study")) { + const selected = this.getValue(); + + if (this.isLocked() && selected) { + this.setValue(false); + } + + const tick = this.getChildControl("tick-selected"); + tick.setVisibility(selected ? "visible" : "excluded"); + + const untick = this.getChildControl("tick-unselected"); + untick.setVisibility(selected ? "excluded" : "visible"); + } else { + this.__showMenuOnly(); + } + }, + + __showMenuOnly: function() { const menuButton = this.getChildControl("menu-button"); - menuButton.setVisibility(on ? "excluded" : "visible"); + menuButton.setVisibility("visible"); + const tick = this.getChildControl("tick-selected"); + tick.setVisibility("excluded"); + const untick = this.getChildControl("tick-unselected"); + untick.setVisibility("excluded"); }, // overridden @@ -134,7 +176,6 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { width: this.self().MENU_BTN_WIDTH, height: this.self().MENU_BTN_WIDTH, icon: "@FontAwesome5Solid/ellipsis-v/14", - zIndex: this.self().MENU_BTN_Z, focusable: false }); osparc.utils.Utils.setIdToWidget(control, "studyItemMenuButton"); @@ -144,23 +185,28 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { }); break; case "tick-unselected": - control = new qx.ui.basic.Image("@FontAwesome5Solid/circle/16").set({ - zIndex: this.self().MENU_BTN_Z -1 - }); + control = new qx.ui.basic.Image("@FontAwesome5Solid/circle/16"); this._add(control, { top: 4, right: 4 }); break; case "tick-selected": - control = new qx.ui.basic.Image("@FontAwesome5Solid/check-circle/16").set({ - zIndex: this.self().MENU_BTN_Z -1 - }); + control = new qx.ui.basic.Image("@FontAwesome5Solid/check-circle/16"); this._add(control, { top: 4, right: 4 }); break; + case "lock": + control = new osparc.component.widget.Thumbnail("@FontAwesome5Solid/lock/70"); + this._add(control, { + top: 0, + right: 0, + bottom: 0, + left: 0 + }); + break; } return control || this.base(arguments, id); @@ -230,20 +276,18 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { const store = osparc.store.Store.getInstance(); Promise.all([ store.getGroupsAll(), - store.getGroupsMe(), store.getVisibleMembers(), store.getGroupsOrganizations() ]) .then(values => { const all = values[0]; - const me = values[1]; const orgMembs = []; - const orgMembers = values[2]; + const orgMembers = values[1]; for (const gid of Object.keys(orgMembers)) { orgMembs.push(orgMembers[gid]); } - const orgs = values.length === 4 ? values[3] : []; - const groups = [[me], orgMembs, orgs, [all]]; + const orgs = values.length === 3 ? values[2] : []; + const groups = [orgMembs, orgs, [all]]; this.__setSharedIcon(image, value, groups); }); } @@ -272,13 +316,12 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { } switch (i) { case 0: - case 1: image.setSource(this.self().SHARED_USER); break; - case 2: + case 1: image.setSource(this.self().SHARED_ORGS); break; - case 3: + case 2: image.setSource(this.self().SHARED_ALL); break; } @@ -289,14 +332,19 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { return; } - let hintText = ""; + const sharedGrpLabels = []; + const maxItems = 6; for (let i=0; i 6) { - hintText += "..."; + if (i > maxItems) { + sharedGrpLabels.push("..."); break; } - hintText += (sharedGrps[i]["label"] + "
"); + const sharedGrpLabel = sharedGrps[i]["label"]; + if (!sharedGrpLabels.includes(sharedGrpLabel)) { + sharedGrpLabels.push(sharedGrpLabel); + } } + const hintText = sharedGrpLabels.join("
"); const hint = new osparc.ui.hint.Hint(image, hintText); image.addListener("mouseover", () => hint.show(), this); image.addListener("mouseout", () => hint.exclude(), this); @@ -314,6 +362,49 @@ qx.Class.define("osparc.dashboard.StudyBrowserButtonItem", { } }, + _applyState: function(state) { + const locked = ("locked" in state) ? state["locked"]["value"] : false; + if (locked) { + this.setLocked(state["locked"]["value"]); + const owner = state["locked"]["owner"]; + this.setLockedBy(osparc.utils.Utils.firstsUp(owner["first_name"], owner["last_name"])); + } else { + this.setLocked(false); + this.setLockedBy(null); + } + }, + + _applyLocked: function(locked) { + this.set({ + cursor: locked ? "not-allowed" : "pointer" + }); + + this._getChildren().forEach(item => { + item.setOpacity(locked ? 0.4 : 1.0); + }); + + const lock = this.getChildControl("lock"); + lock.setOpacity(1.0); + lock.setVisibility(locked ? "visible" : "excluded"); + + [ + "tick-selected", + "tick-unselected", + "menu-button" + ].forEach(childName => { + const child = this.getChildControl(childName); + child.set({ + enabled: !locked + }); + }); + }, + + _applyLockedBy: function(lockedBy) { + this.set({ + toolTipText: lockedBy ? (lockedBy + this.tr(" is using it")) : null + }); + }, + _shouldApplyFilter: function(data) { if (data.text) { const checks = [ diff --git a/services/web/client/source/class/osparc/data/Resources.js b/services/web/client/source/class/osparc/data/Resources.js index d1d4006f00a..d33faec0161 100644 --- a/services/web/client/source/class/osparc/data/Resources.js +++ b/services/web/client/source/class/osparc/data/Resources.js @@ -93,6 +93,11 @@ qx.Class.define("osparc.data.Resources", { method: "POST", url: statics.API + "/projects/{projectId}:close" }, + state: { + useCache: false, + method: "GET", + url: statics.API + "/projects/{projectId}/state" + }, post: { method: "POST", url: statics.API + "/projects" @@ -546,14 +551,20 @@ qx.Class.define("osparc.data.Resources", { res.addListenerOnce(endpoint + "Error", e => { let message = null; + let status = null; if (e.getData().error) { const logs = e.getData().error.logs || null; if (logs && logs.length) { message = logs[0].message; } + status = e.getData().error.status; } res.dispose(); - reject(Error(message ? message : `Error while fetching ${resource}`)); + const err = Error(message ? message : `Error while fetching ${resource}`); + if (status) { + err.status = status; + } + reject(err); }); res[endpoint](params.url || null, params.data || null); diff --git a/services/web/client/source/class/osparc/data/model/Study.js b/services/web/client/source/class/osparc/data/model/Study.js index 378667cc960..3c1770f3cff 100644 --- a/services/web/client/source/class/osparc/data/model/Study.js +++ b/services/web/client/source/class/osparc/data/model/Study.js @@ -141,6 +141,7 @@ qx.Class.define("osparc.data.model.Study", { tags: [] }; }, + updateStudy: function(params) { return osparc.data.Resources.fetch("studies", "put", { url: { @@ -151,6 +152,22 @@ qx.Class.define("osparc.data.model.Study", { qx.event.message.Bus.getInstance().dispatchByName("updateStudy", data); return data; }); + }, + + getProperties: function() { + return Object.keys(qx.util.PropertyUtil.getProperties(osparc.data.model.Study)); + }, + + // deep clones object with study-only properties + deepCloneStudyObject: function(src) { + const studyObject = osparc.utils.Utils.deepCloneObject(src); + const studyPropKeys = osparc.data.model.Study.getProperties(); + Object.keys(studyObject).forEach(key => { + if (!studyPropKeys.includes(key)) { + delete studyObject[key]; + } + }); + return studyObject; } }, @@ -166,9 +183,7 @@ qx.Class.define("osparc.data.model.Study", { }, data: osparc.utils.Utils.getClientSessionID() }; - osparc.data.Resources.fetch("studies", "open", params) - .then(data => this.getWorkbench().initWorkbench()) - .catch(err => console.error(err)); + return osparc.data.Resources.fetch("studies", "open", params); }, closeStudy: function() { @@ -190,14 +205,14 @@ qx.Class.define("osparc.data.model.Study", { serializeStudy: function() { let jsonObject = {}; - const properties = this.constructor.$$properties; - for (let key in properties) { + const propertyKeys = this.self().getProperties(); + propertyKeys.forEach(key => { const value = key === "workbench" ? this.getWorkbench().serializeWorkbench() : this.get(key); if (value !== null) { // only put the value in the payload if there is a value jsonObject[key] = value; } - } + }); return jsonObject; }, diff --git a/services/web/client/source/class/osparc/desktop/MainPage.js b/services/web/client/source/class/osparc/desktop/MainPage.js index 52e975202db..27097492545 100644 --- a/services/web/client/source/class/osparc/desktop/MainPage.js +++ b/services/web/client/source/class/osparc/desktop/MainPage.js @@ -155,6 +155,9 @@ qx.Class.define("osparc.desktop.MainPage", { const elements = ev.getData(); this.__navBar.setPathButtons(elements); }, this); + this.__studyEditor.addListener("studyIsLocked", () => { + this.__showDashboard(); + }, this); this.__studyEditor.addListener("studySaved", ev => { const wasSaved = ev.getData(); diff --git a/services/web/client/source/class/osparc/desktop/StudyEditor.js b/services/web/client/source/class/osparc/desktop/StudyEditor.js index 6cc7b3f3580..650678073cd 100644 --- a/services/web/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/web/client/source/class/osparc/desktop/StudyEditor.js @@ -49,6 +49,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { events: { "changeMainViewCaption": "qx.event.type.Data", + "studyIsLocked": "qx.event.type.Event", "studySaved": "qx.event.type.Data" }, @@ -69,12 +70,23 @@ qx.Class.define("osparc.desktop.StudyEditor", { _applyStudy: function(study) { osparc.store.Store.getInstance().setCurrentStudy(study); study.buildWorkbench(); - study.openStudy(); + study.openStudy() + .then(() => { + study.getWorkbench().initWorkbench(); + }) + .catch(err => { + if ("status" in err && err["status"] == 423) { // Locked + const msg = study.getName() + this.tr(" is already opened"); + osparc.component.message.FlashMessenger.getInstance().logAs(msg, "ERROR"); + this.fireEvent("studyIsLocked"); + } else { + console.error(err); + } + }); this.__initViews(); this.__connectEvents(); this.__attachSocketEventHandlers(); this.__startAutoSaveTimer(); - this.__openOneNode(); }, diff --git a/services/web/client/source/class/osparc/store/Store.js b/services/web/client/source/class/osparc/store/Store.js index e0ee27fddf8..a5f0bf8ac1b 100644 --- a/services/web/client/source/class/osparc/store/Store.js +++ b/services/web/client/source/class/osparc/store/Store.js @@ -197,6 +197,87 @@ qx.Class.define("osparc.store.Store", { } }, + getStudyWState: function(studyId, reload = false) { + return new Promise((resolve, reject) => { + const studiesWStateCache = this.getStudies(); + const idx = studiesWStateCache.findIndex(studyWStateCache => studyWStateCache["uuid"] === studyId); + if (!reload && idx !== -1) { + resolve(studiesWStateCache[idx]); + return; + } + const params = { + url: { + "projectId": studyId + } + }; + osparc.data.Resources.getOne("studies", params) + .then(study => { + osparc.data.Resources.fetch("studies", "state", params) + .then(state => { + study["locked"] = state["locked"]; + if (idx === -1) { + studiesWStateCache.push(study); + } else { + studiesWStateCache[idx] = study; + } + resolve(study); + }) + .catch(er => { + console.error(er); + reject(); + }); + }) + .catch(err => { + console.error(err); + reject(); + }); + }); + }, + + /** + * This function provides the list of studies with their state + * @param {Boolean} reload ? + */ + getStudiesWState: function(reload = false) { + return new Promise((resolve, reject) => { + const studiesWStateCache = this.getStudies(); + if (!reload && studiesWStateCache.length) { + resolve(studiesWStateCache); + return; + } + studiesWStateCache.length = 0; + osparc.data.Resources.get("studies") + .then(studies => { + const studiesWStatePromises = []; + studies.forEach(study => { + const params = { + url: { + "projectId": study.uuid + } + }; + studiesWStatePromises.push(osparc.data.Resources.fetch("studies", "state", params)); + }); + Promise.all(studiesWStatePromises) + .then(states => { + states.forEach((state, idx) => { + const study = studies[idx]; + study["locked"] = state["locked"]; + studiesWStateCache.push(study); + }); + resolve(studiesWStateCache); + }) + .catch(er => { + console.error(er); + reject(); + }); + }) + .catch(err => { + console.error(err); + reject(); + }); + }); + }, + /** * This functions does the needed processing in order to have a working list of services and DAGs. * @param {Boolean} reload ? @@ -269,27 +350,24 @@ qx.Class.define("osparc.store.Store", { }); }, - getVisibleMembers: function() { - const reachableMembers = this.getReachableMembers(); + getVisibleMembers: function(reload = false) { return new Promise((resolve, reject) => { + const reachableMembers = this.getReachableMembers(); + if (!reload && Object.keys(reachableMembers).length) { + resolve(reachableMembers); + return; + } osparc.data.Resources.get("organizations") .then(resp => { const orgMembersPromises = []; const orgs = resp["organizations"]; orgs.forEach(org => { - orgMembersPromises.push( - new Promise((resolve2, reject2) => { - const params = { - url: { - "gid": org["gid"] - } - }; - osparc.data.Resources.get("organizationMembers", params) - .then(orgMembers => { - resolve2(orgMembers); - }); - }) - ); + const params = { + url: { + "gid": org["gid"] + } + }; + orgMembersPromises.push(osparc.data.Resources.get("organizationMembers", params)); }); Promise.all(orgMembersPromises) .then(orgMemberss => {