diff --git a/services/static-webserver/client/source/class/osparc/data/model/IframeHandler.js b/services/static-webserver/client/source/class/osparc/data/model/IframeHandler.js index 850672e96572..29eb686f241d 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/IframeHandler.js +++ b/services/static-webserver/client/source/class/osparc/data/model/IframeHandler.js @@ -117,7 +117,7 @@ qx.Class.define("osparc.data.model.IframeHandler", { } const node = this.getNode(); - const thumbnail = node.getMetaData()["thumbnail"]; + const thumbnail = node.getMetadata()["thumbnail"]; if (thumbnail) { loadingPage.setLogo(thumbnail); } @@ -141,7 +141,7 @@ qx.Class.define("osparc.data.model.IframeHandler", { status = node.getStatus().getInteractive(); } const statusText = status ? (status.charAt(0).toUpperCase() + status.slice(1)) : this.tr("Starting"); - const metadata = node.getMetaData(); + const metadata = node.getMetadata(); const versionDisplay = osparc.service.Utils.extractVersionDisplay(metadata); return statusText + " " + node.getLabel() + " v" + versionDisplay + ""; }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/Node.js b/services/static-webserver/client/source/class/osparc/data/model/Node.js index 76b4200e77dc..7e0ea962091a 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Node.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Node.js @@ -41,14 +41,13 @@ qx.Class.define("osparc.data.model.Node", { /** * @param study {osparc.data.model.Study} Study or Serialized Study Object - * @param metadata {Object} service's metadata + * @param key {String} unique key of the service represented by the node + * @param version {String} version of the service represented by the node * @param nodeId {String} uuid of the service represented by the node (not needed for new Nodes) */ - construct: function(study, metadata, nodeId) { + construct: function(study, key, version, nodeId) { this.base(arguments); - this.__metaData = metadata; - this.setOutputs({}); this.__inputNodes = []; this.__inputsRequired = []; @@ -57,12 +56,10 @@ qx.Class.define("osparc.data.model.Node", { } this.set({ nodeId: nodeId || osparc.utils.Utils.uuidV4(), - key: metadata["key"], - version: metadata["version"], + key, + version, status: new osparc.data.model.NodeStatus(this) }); - - this.populateWithMetadata(); }, properties: { @@ -76,14 +73,12 @@ qx.Class.define("osparc.data.model.Node", { key: { check: "String", nullable: true, - apply: "__applyNewMetaData" }, version: { check: "String", nullable: true, event: "changeVersion", - apply: "__applyNewMetaData" }, nodeId: { @@ -91,6 +86,14 @@ qx.Class.define("osparc.data.model.Node", { nullable: false }, + metadata: { + check: "Object", + init: null, + nullable: false, + event: "changeMetadata", + apply: "__applyMetadata", + }, + label: { check: "String", init: "Node", @@ -199,8 +202,8 @@ qx.Class.define("osparc.data.model.Node", { "retrieveInputs": "qx.event.type.Data", "keyChanged": "qx.event.type.Event", "changePosition": "qx.event.type.Data", - "createEdge": "qx.event.type.Data", - "removeEdge": "qx.event.type.Data", + "edgeCreated": "qx.event.type.Data", + "edgeRemoved": "qx.event.type.Data", "fileRequested": "qx.event.type.Data", "parameterRequested": "qx.event.type.Data", "filePickerRequested": "qx.event.type.Data", @@ -272,6 +275,10 @@ qx.Class.define("osparc.data.model.Node", { return (metadata && metadata.type && metadata.type === "computational"); }, + isUnknown: function(metadata) { + return (metadata && metadata.key && metadata.key === osparc.store.Services.UNKNOWN_SERVICE_KEY); + }, + isUpdatable: function(metadata) { return osparc.service.Utils.isUpdatable(metadata); }, @@ -353,7 +360,6 @@ qx.Class.define("osparc.data.model.Node", { }, members: { - __metaData: null, __inputNodes: null, __inputsRequired: null, __settingsForm: null, @@ -371,7 +377,7 @@ qx.Class.define("osparc.data.model.Node", { }, isInKey: function(str) { - if (this.getMetaData() === null) { + if (this.getMetadata() === null) { return false; } if (this.getKey() === null) { @@ -381,60 +387,51 @@ qx.Class.define("osparc.data.model.Node", { }, isFilePicker: function() { - return osparc.data.model.Node.isFilePicker(this.getMetaData()); + return osparc.data.model.Node.isFilePicker(this.getMetadata()); }, isParameter: function() { - return osparc.data.model.Node.isParameter(this.getMetaData()); + return osparc.data.model.Node.isParameter(this.getMetadata()); }, isIterator: function() { - return osparc.data.model.Node.isIterator(this.getMetaData()); + return osparc.data.model.Node.isIterator(this.getMetadata()); }, isProbe: function() { - return osparc.data.model.Node.isProbe(this.getMetaData()); + return osparc.data.model.Node.isProbe(this.getMetadata()); }, isDynamic: function() { - return osparc.data.model.Node.isDynamic(this.getMetaData()); + return osparc.data.model.Node.isDynamic(this.getMetadata()); }, isComputational: function() { - return osparc.data.model.Node.isComputational(this.getMetaData()); + return osparc.data.model.Node.isComputational(this.getMetadata()); + }, + + isUnknown: function() { + return osparc.data.model.Node.isUnknown(this.getMetadata()); }, isUpdatable: function() { - return osparc.data.model.Node.isUpdatable(this.getMetaData()); + return osparc.data.model.Node.isUpdatable(this.getMetadata()); }, isDeprecated: function() { - return osparc.data.model.Node.isDeprecated(this.getMetaData()); + return osparc.data.model.Node.isDeprecated(this.getMetadata()); }, isRetired: function() { - return osparc.data.model.Node.isRetired(this.getMetaData()); + return osparc.data.model.Node.isRetired(this.getMetadata()); }, hasBootModes: function() { - return osparc.data.model.Node.hasBootModes(this.getMetaData()); + return osparc.data.model.Node.hasBootModes(this.getMetadata()); }, getMinVisibleInputs: function() { - return osparc.data.model.Node.getMinVisibleInputs(this.getMetaData()); - }, - - __applyNewMetaData: function(newV, oldV) { - if (oldV !== null) { - const metadata = osparc.store.Services.getMetadata(this.getKey(), this.getVersion()); - if (metadata) { - this.__metaData = metadata; - } - } - }, - - getMetaData: function() { - return this.__metaData; + return osparc.data.model.Node.getMinVisibleInputs(this.getMetadata()); }, hasPropsForm: function() { @@ -482,8 +479,26 @@ qx.Class.define("osparc.data.model.Node", { return Object.keys(this.getOutputs()).length; }, - populateWithMetadata: function() { - const metadata = this.__metaData; + fetchMetadataAndPopulate: function(nodeData, nodeUiData) { + this.__initNodeData = nodeData; + this.__initNodeUiData = nodeUiData; + return osparc.store.Services.getService(this.getKey(), this.getVersion()) + .then(serviceMetadata => { + this.setMetadata(serviceMetadata); + this.populateNodeData(nodeData); + // old place to store the position + this.populateNodeUIData(nodeData); + // new place to store the position and marker + this.populateNodeUIData(nodeUiData); + }) + .catch(err => { + console.log(err); + const errorMsg = qx.locale.Manager.tr("Service metadata missing"); + osparc.FlashMessenger.logError(errorMsg); + }); + }, + + __applyMetadata: function(metadata) { if (metadata) { if (metadata.name) { this.setLabel(metadata.name); @@ -496,9 +511,13 @@ qx.Class.define("osparc.data.model.Node", { if (this.getPropsForm()) { this.getPropsForm().makeInputsDynamic(); } + } else { + this.setInputs({}); } if (metadata.outputs) { this.setOutputs(metadata.outputs); + } else { + this.setOutputs({}); } } }, @@ -508,17 +527,15 @@ qx.Class.define("osparc.data.model.Node", { if (nodeData.label) { this.setLabel(nodeData.label); } - this.populateInputOutputData(nodeData); + this.__populateInputOutputData(nodeData); this.populateStates(nodeData); if (nodeData.bootOptions) { this.setBootOptions(nodeData.bootOptions); } } - if (this.isParameter()) { this.__initParameter(); } - if (osparc.store.Store.getInstance().getCurrentStudy()) { // do not initialize the logger and iframe if the study isn't open this.__initLogger(); @@ -527,15 +544,17 @@ qx.Class.define("osparc.data.model.Node", { }, populateNodeUIData: function(nodeUIData) { - if ("position" in nodeUIData) { - this.setPosition(nodeUIData.position); - } - if ("marker" in nodeUIData) { - this.addMarker(nodeUIData.marker); + if (nodeUIData) { + if ("position" in nodeUIData) { + this.setPosition(nodeUIData.position); + } + if ("marker" in nodeUIData) { + this.addMarker(nodeUIData.marker); + } } }, - populateInputOutputData: function(nodeData) { + __populateInputOutputData: function(nodeData) { this.__setInputData(nodeData.inputs); this.__setInputUnits(nodeData.inputsUnits); if (this.getPropsForm()) { @@ -591,23 +610,6 @@ qx.Class.define("osparc.data.model.Node", { __applyPropsForm: function(propsForm) { osparc.utils.Utils.setIdToWidget(propsForm, "settingsForm_" + this.getNodeId()); - - const checkIsPipelineRunning = () => { - const isPipelineRunning = this.getStudy().isPipelineRunning(); - this.getPropsForm().setEnabled(!isPipelineRunning); - }; - this.getStudy().addListener("changeState", () => checkIsPipelineRunning(), this); - - // potentially disabling the inputs form might have side effects if the deserialization is not over - if (this.getWorkbench().isDeserialized()) { - checkIsPipelineRunning(); - } else { - this.getWorkbench().addListener("changeDeserialized", e => { - if (e.getData()) { - checkIsPipelineRunning(); - } - }, this); - } }, /** @@ -734,8 +736,8 @@ qx.Class.define("osparc.data.model.Node", { __setInputData: function(inputs) { if (this.__settingsForm && inputs) { - const inputData = {}; const inputLinks = {}; + const inputData = {}; const inputsCopy = osparc.utils.Utils.deepCloneObject(inputs); for (let key in inputsCopy) { if (osparc.utils.Ports.isDataALink(inputsCopy[key])) { @@ -831,8 +833,8 @@ qx.Class.define("osparc.data.model.Node", { // errors to port if (loc.length > 2) { const portKey = loc[2]; - if (this.hasInputs() && portKey in this.getMetaData()["inputs"]) { - errorMsgData["msg"] = this.getMetaData()["inputs"][portKey]["label"] + ": " + errorMsgData["msg"]; + if (this.hasInputs() && portKey in this.getMetadata()["inputs"]) { + errorMsgData["msg"] = this.getMetadata()["inputs"][portKey]["label"] + ": " + errorMsgData["msg"]; } else { errorMsgData["msg"] = portKey + ": " + errorMsgData["msg"]; } @@ -845,7 +847,7 @@ qx.Class.define("osparc.data.model.Node", { }); } else if (this.hasInputs()) { // reset port errors - Object.keys(this.getMetaData()["inputs"]).forEach(portKey => { + Object.keys(this.getMetadata()["inputs"]).forEach(portKey => { this.getPropsForm().setPortErrorMessage(portKey, null); }); } @@ -1135,7 +1137,7 @@ qx.Class.define("osparc.data.model.Node", { checkState: function() { if (this.isDynamic()) { - const metadata = this.getMetaData(); + const metadata = this.getMetadata(); const msg = "Starting " + metadata.key + ":" + metadata.version + "..."; const msgData = { nodeId: this.getNodeId(), @@ -1154,7 +1156,7 @@ qx.Class.define("osparc.data.model.Node", { stopDynamicService: function() { if (this.isDynamic()) { - const metadata = this.getMetaData(); + const metadata = this.getMetadata(); const msg = "Stopping " + metadata.key + ":" + metadata.version + "..."; const msgData = { nodeId: this.getNodeId(), @@ -1285,8 +1287,11 @@ qx.Class.define("osparc.data.model.Node", { if (newMetadata) { const value = this.__getInputData()["linspace_start"]; const label = this.getLabel(); - this.setKey(newMetadata["key"]); - this.populateWithMetadata(); + this.set({ + key: newMetadata["key"], + version: newMetadata["version"], + }); + this.setMetadata(newMetadata); this.populateNodeData(); this.setLabel(label); osparc.node.ParameterEditor.setParameterOutputValue(this, value); @@ -1298,12 +1303,15 @@ qx.Class.define("osparc.data.model.Node", { if (!["int"].includes(type)) { return; } - const metadata = osparc.store.Services.getLatest("simcore/services/frontend/data-iterator/int-range") - if (metadata) { + const newMetadata = osparc.store.Services.getLatest("simcore/services/frontend/data-iterator/int-range") + if (newMetadata) { const value = this.__getOutputData("out_1"); const label = this.getLabel(); - this.setKey(metadata["key"]); - this.populateWithMetadata(); + this.set({ + key: newMetadata["key"], + version: newMetadata["version"], + }); + this.setMetadata(newMetadata); this.populateNodeData(); this.setLabel(label); this.__setInputData({ @@ -1459,7 +1467,7 @@ qx.Class.define("osparc.data.model.Node", { case "inputNodes": if (op === "add") { const inputNodeId = value; - this.fireDataEvent("createEdge", { + this.fireDataEvent("edgeCreated", { nodeId1: inputNodeId, nodeId2: this.getNodeId(), }); @@ -1468,7 +1476,7 @@ qx.Class.define("osparc.data.model.Node", { const index = path.split("/")[4]; // make sure index is valid if (index >= 0 && index < this.__inputNodes.length) { - this.fireDataEvent("removeEdge", { + this.fireDataEvent("edgeRemoved", { nodeId1: this.__inputNodes[index], nodeId2: this.getNodeId(), }); diff --git a/services/static-webserver/client/source/class/osparc/data/model/NodeUnknown.js b/services/static-webserver/client/source/class/osparc/data/model/NodeUnknown.js new file mode 100644 index 000000000000..8eae34e51fd3 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/data/model/NodeUnknown.js @@ -0,0 +1,57 @@ +/* ************************************************************************ + + 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) + +************************************************************************ */ + +/** + * Class that stores Node data without a known metadata. + */ + +qx.Class.define("osparc.data.model.NodeUnknown", { + extend: osparc.data.model.Node, + + /** + * @param study {osparc.data.model.Study} Study or Serialized Study Object + * @param key {String} service's key + * @param version {String} service's version + * @param nodeId {String} uuid of the service represented by the node (not needed for new Nodes) + */ + construct: function(study, key, version, nodeId) { + // use the unknown metadata + const metadata = osparc.store.Services.getUnknownServiceMetadata(); + this.base(arguments, study, metadata, nodeId); + + // but keep the original key and version + if (key && version) { + this.set({ + key, + version, + }); + } + }, + + members: { + // override + serialize: function() { + /* + if (this.getKey() === osparc.store.Services.UNKNOWN_SERVICE_KEY) { + return null; + } + */ + + return null; + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Service.js b/services/static-webserver/client/source/class/osparc/data/model/Service.js index d260144d4c05..a9b5382c63d0 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Service.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Service.js @@ -31,18 +31,18 @@ qx.Class.define("osparc.data.model.Service", { this.set({ key: serviceData.key, version: serviceData.version, - versionDisplay: serviceData.versionDisplay, + versionDisplay: serviceData.versionDisplay || null, name: serviceData.name, - description: serviceData.description, - thumbnail: serviceData.thumbnail, - serviceType: serviceData.type, - contact: serviceData.contact, - authors: serviceData.authors, - owner: serviceData.owner || "", + description: serviceData.description || null, + thumbnail: serviceData.thumbnail || null, + serviceType: serviceData.type || null, + contact: serviceData.contact || null, + authors: serviceData.authors || null, + owner: serviceData.owner || null, accessRights: serviceData.accessRights, - bootOptions: serviceData.bootOptions, + bootOptions: serviceData.bootOptions || null, classifiers: serviceData.classifiers || [], - quality: serviceData.quality || null, + quality: serviceData.quality || {}, xType: serviceData.xType || null, hits: serviceData.hits || 0, }); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Study.js b/services/static-webserver/client/source/class/osparc/data/model/Study.js index 8f9e82b98512..03b0f0dd4c8c 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Study.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Study.js @@ -574,7 +574,7 @@ qx.Class.define("osparc.data.model.Study", { // The frontend controls its output values, progress and states. // If a File Picker is uploading a file, the backend could override the current state with some older state. if (node) { - if (nodeData && !osparc.data.model.Node.isFrontend(node.getMetaData())) { + if (nodeData && !osparc.data.model.Node.isFrontend(node.getMetadata())) { node.setOutputData(nodeData.outputs); if ("progress" in nodeData) { const progress = Number.parseInt(nodeData["progress"]); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Workbench.js b/services/static-webserver/client/source/class/osparc/data/model/Workbench.js index eed47e42c1c7..8bb1f1a056b6 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Workbench.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Workbench.js @@ -54,6 +54,7 @@ qx.Class.define("osparc.data.model.Workbench", { "projectDocumentChanged": "qx.event.type.Data", "restartAutoSaveTimer": "qx.event.type.Event", "pipelineChanged": "qx.event.type.Event", + "nodeAdded": "qx.event.type.Data", "nodeRemoved": "qx.event.type.Data", "reloadModel": "qx.event.type.Event", "retrieveInputs": "qx.event.type.Data", @@ -111,6 +112,63 @@ qx.Class.define("osparc.data.model.Workbench", { this.__workbenchUIInitData = null; }, + __deserialize: function(workbenchInitData, uiData = {}) { + const nodeDatas = {}; + const nodeUiDatas = {}; + for (const nodeId in workbenchInitData) { + const nodeData = workbenchInitData[nodeId]; + nodeDatas[nodeId] = nodeData; + if (uiData["workbench"] && nodeId in uiData["workbench"]) { + nodeUiDatas[nodeId] = uiData["workbench"][nodeId]; + } + } + this.__deserializeNodes(nodeDatas, nodeUiDatas) + .then(() => { + this.__deserializeEdges(workbenchInitData); + this.setDeserialized(true); + }); + }, + + __deserializeNodes: function(nodeDatas, nodeUiDatas) { + const nodesPromises = []; + for (const nodeId in nodeDatas) { + const nodeData = nodeDatas[nodeId]; + const nodeUiData = nodeUiDatas[nodeId]; + const node = this.__createNode(nodeData["key"], nodeData["version"], nodeId); + nodesPromises.push(node.fetchMetadataAndPopulate(nodeData, nodeUiData)); + } + return Promise.allSettled(nodesPromises); + }, + + __createNode: function(key, version, nodeId) { + const node = new osparc.data.model.Node(this.getStudy(), key, version, nodeId); + this.__addNode(node); + this.__initNodeSignals(node); + osparc.utils.Utils.localCache.serviceToFavs(key); + return node; + }, + + + __deserializeEdges: function(workbenchData) { + for (const nodeId in workbenchData) { + const node = this.getNode(nodeId); + if (node === null) { + continue; + } + const nodeData = workbenchData[nodeId]; + const inputNodeIds = nodeData.inputNodes || []; + inputNodeIds.forEach(inputNodeId => { + const inputNode = this.getNode(inputNodeId); + if (inputNode === null) { + return; + } + const edge = new osparc.data.model.Edge(null, inputNode, node); + this.addEdge(edge); + node.addInputNode(inputNodeId); + }); + } + }, + // starts the dynamic services initWorkbench: function() { const allModels = this.getNodes(); @@ -273,21 +331,13 @@ qx.Class.define("osparc.data.model.Workbench", { nodeRight.setInputConnected(true); }, - __createNode: function(study, metadata, uuid) { - const node = new osparc.data.model.Node(study, metadata, uuid); - if (osparc.utils.Utils.eventDrivenPatch()) { - node.listenToChanges(); - node.addListener("projectDocumentChanged", e => this.fireDataEvent("projectDocumentChanged", e.getData()), this); + createUnknownNode: function(nodeId) { + if (nodeId === undefined) { + nodeId = osparc.utils.Utils.uuidV4(); } - node.addListener("keyChanged", () => this.fireEvent("reloadModel"), this); - node.addListener("changeInputNodes", () => this.fireDataEvent("pipelineChanged"), this); - node.addListener("reloadModel", () => this.fireEvent("reloadModel"), this); - node.addListener("updateStudyDocument", () => this.fireEvent("updateStudyDocument"), this); - osparc.utils.Utils.localCache.serviceToFavs(metadata.key); - - this.__initNodeSignals(node); + const node = new osparc.data.model.NodeUnknown(this.getStudy(), null, null, nodeId); this.__addNode(node); - + node.populateNodeData(); return node; }, @@ -315,16 +365,16 @@ qx.Class.define("osparc.data.model.Workbench", { }; try { - const metadata = await osparc.store.Services.getService(key, version); const resp = await osparc.data.Resources.fetch("studies", "addNode", params); const nodeId = resp["node_id"]; this.fireEvent("restartAutoSaveTimer"); - const node = this.__createNode(this.getStudy(), metadata, nodeId); - node.populateNodeData(); - this.__giveUniqueNameToNode(node, node.getLabel()); - node.checkState(); - + const node = this.__createNode(key, version, nodeId); + node.fetchMetadataAndPopulate() + .then(() => { + this.__giveUniqueNameToNode(node, node.getLabel()); + node.checkState(); + }); return node; } catch (err) { let errorMsg = ""; @@ -344,50 +394,57 @@ qx.Class.define("osparc.data.model.Workbench", { }, __initNodeSignals: function(node) { - if (node) { - node.addListener("showInLogger", e => this.fireDataEvent("showInLogger", e.getData()), this); - node.addListener("retrieveInputs", e => this.fireDataEvent("retrieveInputs", e.getData()), this); - node.addListener("fileRequested", e => this.fireDataEvent("fileRequested", e.getData()), this); - node.addListener("filePickerRequested", e => { - const { - portId, - nodeId, - file - } = e.getData(); - this.__filePickerNodeRequested(nodeId, portId, file); - }, this); - node.addListener("parameterRequested", e => { - const { - portId, - nodeId - } = e.getData(); - this.__parameterNodeRequested(nodeId, portId); - }, this); - node.addListener("probeRequested", e => { - const { - portId, - nodeId - } = e.getData(); - this.__probeNodeRequested(nodeId, portId); - }, this); - node.addListener("fileUploaded", () => { - // downstream nodes might have started downloading file picker's output. - // show feedback to the user - const downstreamNodes = this.__getDownstreamNodes(node); - downstreamNodes.forEach(downstreamNode => { - downstreamNode.getPortIds().forEach(portId => { - const link = downstreamNode.getLink(portId); - if (link && link["nodeUuid"] === node.getNodeId() && link["output"] === "outFile") { - // connected to file picker's output - setTimeout(() => { - // start retrieving state after 2" - downstreamNode.retrieveInputs(portId); - }, 2000); - } - }); - }); - }, this); + if (osparc.utils.Utils.eventDrivenPatch()) { + node.listenToChanges(); + node.addListener("projectDocumentChanged", e => this.fireDataEvent("projectDocumentChanged", e.getData()), this); } + node.addListener("keyChanged", () => this.fireEvent("reloadModel"), this); + node.addListener("changeInputNodes", () => this.fireDataEvent("pipelineChanged"), this); + node.addListener("reloadModel", () => this.fireEvent("reloadModel"), this); + node.addListener("updateStudyDocument", () => this.fireEvent("updateStudyDocument"), this); + + node.addListener("showInLogger", e => this.fireDataEvent("showInLogger", e.getData()), this); + node.addListener("retrieveInputs", e => this.fireDataEvent("retrieveInputs", e.getData()), this); + node.addListener("fileRequested", e => this.fireDataEvent("fileRequested", e.getData()), this); + node.addListener("filePickerRequested", e => { + const { + portId, + nodeId, + file + } = e.getData(); + this.__filePickerNodeRequested(nodeId, portId, file); + }, this); + node.addListener("parameterRequested", e => { + const { + portId, + nodeId + } = e.getData(); + this.__parameterNodeRequested(nodeId, portId); + }, this); + node.addListener("probeRequested", e => { + const { + portId, + nodeId + } = e.getData(); + this.__probeNodeRequested(nodeId, portId); + }, this); + node.addListener("fileUploaded", () => { + // downstream nodes might have started downloading file picker's output. + // show feedback to the user + const downstreamNodes = this.__getDownstreamNodes(node); + downstreamNodes.forEach(downstreamNode => { + downstreamNode.getPortIds().forEach(portId => { + const link = downstreamNode.getLink(portId); + if (link && link["nodeUuid"] === node.getNodeId() && link["output"] === "outFile") { + // connected to file picker's output + setTimeout(() => { + // start retrieving state after 2" + downstreamNode.retrieveInputs(portId); + }, 2000); + } + }); + }); + }, this); }, getFreePosition: function(node, toTheLeft = true) { @@ -476,7 +533,7 @@ qx.Class.define("osparc.data.model.Workbench", { const requesterNode = this.getNode(nodeId); // create a new ParameterNode - const type = osparc.utils.Ports.getPortType(requesterNode.getMetaData()["inputs"], portId); + const type = osparc.utils.Ports.getPortType(requesterNode.getMetadata()["inputs"], portId); const parameterMetadata = osparc.store.Services.getParameterMetadata(type); if (parameterMetadata) { const parameterNode = await this.createNode(parameterMetadata["key"], parameterMetadata["version"]); @@ -505,8 +562,8 @@ qx.Class.define("osparc.data.model.Workbench", { const requesterNode = this.getNode(nodeId); // create a new ProbeNode - const requesterPortMD = requesterNode.getMetaData()["outputs"][portId]; - const type = osparc.utils.Ports.getPortType(requesterNode.getMetaData()["outputs"], portId); + const requesterPortMD = requesterNode.getMetadata()["outputs"][portId]; + const type = osparc.utils.Ports.getPortType(requesterNode.getMetadata()["outputs"], portId); const probeMetadata = osparc.store.Services.getProbeMetadata(type); if (probeMetadata) { const probeNode = await this.createNode(probeMetadata["key"], probeMetadata["version"]); @@ -536,7 +593,14 @@ qx.Class.define("osparc.data.model.Workbench", { __addNode: function(node) { const nodeId = node.getNodeId(); this.__nodes[nodeId] = node; - this.fireEvent("pipelineChanged"); + const nodeAdded = () => { + this.fireEvent("pipelineChanged"); + }; + if (node.getMetadata()) { + nodeAdded(); + } else { + node.addListenerOnce("changeMetadata", () => nodeAdded(), this); + } }, removeNode: async function(nodeId) { @@ -675,88 +739,6 @@ qx.Class.define("osparc.data.model.Workbench", { } }, - __populateNodesData: function(workbenchData, workbenchUIData) { - Object.entries(workbenchData).forEach(([nodeId, nodeData]) => { - this.getNode(nodeId).populateNodeData(nodeData); - - if ("position" in nodeData) { - // old way for storing the position - this.getNode(nodeId).populateNodeUIData(nodeData); - } - if (workbenchUIData && "workbench" in workbenchUIData && nodeId in workbenchUIData.workbench) { - this.getNode(nodeId).populateNodeUIData(workbenchUIData.workbench[nodeId]); - } - }); - }, - - __deserialize: function(workbenchInitData, workbenchUIInitData) { - this.__deserializeNodes(workbenchInitData, workbenchUIInitData) - .then(() => { - this.__deserializeEdges(workbenchInitData); - workbenchInitData = null; - workbenchUIInitData = null; - this.setDeserialized(true); - }); - }, - - __deserializeNodes: function(workbenchData, workbenchUIData = {}) { - const nodeIds = Object.keys(workbenchData); - const serviceMetadataPromises = []; - nodeIds.forEach(nodeId => { - const nodeData = workbenchData[nodeId]; - serviceMetadataPromises.push(osparc.store.Services.getService(nodeData.key, nodeData.version)); - }); - return Promise.allSettled(serviceMetadataPromises) - .then(results => { - const missing = results.filter(result => result.status === "rejected" || result.value === null) - if (missing.length) { - const errorMsg = qx.locale.Manager.tr("Service metadata missing"); - osparc.FlashMessenger.logError(errorMsg); - return; - } - const values = results.map(result => result.value); - // Create first all the nodes - for (let i=0; i { - const node = this.getNode(nodeId); - this.__giveUniqueNameToNode(node, node.getLabel()); - }); - }); - }, - - __deserializeEdges: function(workbenchData) { - for (const nodeId in workbenchData) { - const nodeData = workbenchData[nodeId]; - const node = this.getNode(nodeId); - if (node === null) { - continue; - } - this.__addInputOutputNodesAndEdges(node, nodeData.inputNodes); - } - }, - - __addInputOutputNodesAndEdges: function(node, inputOutputNodeIds) { - if (inputOutputNodeIds) { - inputOutputNodeIds.forEach(inputOutputNodeId => { - const node1 = this.getNode(inputOutputNodeId); - if (node1 === null) { - return; - } - const edge = new osparc.data.model.Edge(null, node1, node); - this.addEdge(edge); - node.addInputNode(inputOutputNodeId); - }); - } - }, - serialize: function() { if (this.__workbenchInitData !== null) { // workbench is not initialized @@ -836,11 +818,17 @@ qx.Class.define("osparc.data.model.Workbench", { return Promise.all(promises); }, - updateWorkbenchFromPatches: function(workbenchPatches) { + /** + * Update the workbench from the given patches. + * @param workbenchPatches {Array} Array of workbench patches. + * @param uiPatches {Array} Array of UI patches. They might contain info (position) about new nodes. + */ + updateWorkbenchFromPatches: function(workbenchPatches, uiPatches) { // group the patches by nodeId const nodesAdded = []; const nodesRemoved = []; const workbenchPatchesByNode = {}; + const workbenchUiPatchesByNode = {}; workbenchPatches.forEach(workbenchPatch => { const nodeId = workbenchPatch.path.split("/")[2]; @@ -865,10 +853,20 @@ qx.Class.define("osparc.data.model.Workbench", { if (nodesRemoved.length) { this.__removeNodesFromPatches(nodesRemoved, workbenchPatchesByNode); } + // second, add nodes if any if (nodesAdded.length) { // this will call update nodes once finished - this.__addNodesFromPatches(nodesAdded, workbenchPatchesByNode); + nodesAdded.forEach(nodeId => { + const uiPatchFound = uiPatches.find(uiPatch => { + const pathParts = uiPatch.path.split("/"); + return uiPatch.op === "add" && pathParts.length === 4 && pathParts[3] === nodeId; + }); + if (uiPatchFound) { + workbenchUiPatchesByNode[nodeId] = uiPatchFound; + } + }); + this.__addNodesFromPatches(nodesAdded, workbenchPatchesByNode, workbenchUiPatchesByNode); } else { // third, update nodes this.__updateNodesFromPatches(workbenchPatchesByNode); @@ -892,12 +890,8 @@ qx.Class.define("osparc.data.model.Workbench", { }); }, - __addNodesFromPatches: function(nodesAdded, workbenchPatchesByNode) { - // not solved yet, log the user out to avoid issues - qx.core.Init.getApplication().logout(qx.locale.Manager.tr("Potentially conflicting updates coming from a collaborator")); - return; - - const promises = nodesAdded.map(nodeId => { + __addNodesFromPatches: function(nodesAdded, workbenchPatchesByNode, workbenchUiPatchesByNode = {}) { + nodesAdded.forEach(nodeId => { const addNodePatch = workbenchPatchesByNode[nodeId].find(workbenchPatch => { const pathParts = workbenchPatch.path.split("/"); return pathParts.length === 3 && workbenchPatch.op === "add"; @@ -908,18 +902,25 @@ qx.Class.define("osparc.data.model.Workbench", { if (index > -1) { workbenchPatchesByNode[nodeId].splice(index, 1); } - // this is an async operation with an await - return this.createNode(nodeData["key"], nodeData["version"]); + + const nodeUiData = workbenchUiPatchesByNode[nodeId] && workbenchUiPatchesByNode[nodeId]["value"] ? workbenchUiPatchesByNode[nodeId]["value"] : {}; + + const node = this.__createNode(nodeData["key"], nodeData["version"], nodeId); + node.fetchMetadataAndPopulate(nodeData, nodeUiData) + .then(() => { + this.fireDataEvent("nodeAdded", node); + node.checkState(); + // check it was already linked + if (nodeData.inputNodes && nodeData.inputNodes.length > 0) { + nodeData.inputNodes.forEach(inputNodeId => { + node.fireDataEvent("edgeCreated", { + nodeId1: inputNodeId, + nodeId2: nodeId, + }); + }); + } + }); }); - return Promise.all(promises) - .then(nodes => { - // may populate it - // after adding nodes, we can apply the patches - this.__updateNodesFromPatches(workbenchPatchesByNode); - }) - .catch(err => { - console.error("Error adding nodes from patches:", err); - }); }, __updateNodesFromPatches: function(workbenchPatchesByNode) { @@ -933,5 +934,82 @@ qx.Class.define("osparc.data.model.Workbench", { node.updateNodeFromPatch(nodePatches); }); }, + + /** + * @deprecated This method is deprecated and will be removed in a future release. + * Please use `__deserialize` instead for deserializing workbench data. + * Migration: Replace calls to `__deserializeOld` with `__deserialize`. + */ + __deserializeOld: function(workbenchInitData, workbenchUIInitData) { + this.__deserializeNodesOld(workbenchInitData, workbenchUIInitData) + .then(() => { + this.__deserializeEdges(workbenchInitData); + workbenchInitData = null; + workbenchUIInitData = null; + this.setDeserialized(true); + }); + }, + + __deserializeNodesOld: function(workbenchData, workbenchUIData = {}) { + const nodeIds = Object.keys(workbenchData); + const serviceMetadataPromises = []; + nodeIds.forEach(nodeId => { + const nodeData = workbenchData[nodeId]; + serviceMetadataPromises.push(osparc.store.Services.getService(nodeData.key, nodeData.version)); + }); + return Promise.allSettled(serviceMetadataPromises) + .then(results => { + const missing = results.filter(result => result.status === "rejected" || result.value === null) + if (missing.length) { + const errorMsg = qx.locale.Manager.tr("Service metadata missing"); + osparc.FlashMessenger.logError(errorMsg); + return; + } + const values = results.map(result => result.value); + // Create first all the nodes + for (let i=0; i this.fireDataEvent("projectDocumentChanged", e.getData()), this); + } + node.addListener("keyChanged", () => this.fireEvent("reloadModel"), this); + node.addListener("changeInputNodes", () => this.fireDataEvent("pipelineChanged"), this); + node.addListener("reloadModel", () => this.fireEvent("reloadModel"), this); + node.addListener("updateStudyDocument", () => this.fireEvent("updateStudyDocument"), this); + osparc.utils.Utils.localCache.serviceToFavs(metadata["key"]); + + this.__initNodeSignals(node); + this.__addNode(node); + + return node; + }, + + __populateNodesDataOld: function(workbenchData, workbenchUIData) { + Object.entries(workbenchData).forEach(([nodeId, nodeData]) => { + this.getNode(nodeId).populateNodeData(nodeData); + + if ("position" in nodeData) { + // old place to store the position + this.getNode(nodeId).populateNodeUIData(nodeData); + } + if (workbenchUIData && "workbench" in workbenchUIData && nodeId in workbenchUIData["workbench"]) { + // new place to store the position and marker + this.getNode(nodeId).populateNodeUIData(workbenchUIData["workbench"][nodeId]); + } + }); + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js index db2902c0539b..6c41adcdb189 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -160,6 +160,8 @@ qx.Class.define("osparc.desktop.StudyEditor", { __studyEditorIdlingTracker: null, __lastSyncedProjectDocument: null, __lastSyncedProjectVersion: null, + __pendingProjectData: null, + __applyProjectDocumentTimer: null, __updatingStudy: null, __updateThrottled: null, __nodesSlidesTree: null, @@ -334,57 +336,96 @@ qx.Class.define("osparc.desktop.StudyEditor", { if (data["projectId"] === this.getStudy().getUuid()) { if (data["clientSessionId"] && data["clientSessionId"] === osparc.utils.Utils.getClientSessionID()) { // ignore my own updates - console.debug("Ignoring my own projectDocument:updated event", data); + console.debug("ProjectDocument Discarded: My own", data); return; } - - const documentVersion = data["version"]; - if (this.__lastSyncedProjectVersion && documentVersion <= this.__lastSyncedProjectVersion) { - // ignore old updates - console.debug("Ignoring old projectDocument:updated event", data); - return; - } - this.__lastSyncedProjectVersion = documentVersion; - - const updatedStudy = data["document"]; - // curate projectDocument:updated document - this.self().curateBackendProjectDocument(updatedStudy); - - const myStudy = this.getStudy().serialize(); - // curate myStudy - this.self().curateFrontendProjectDocument(myStudy); - - this.__blockUpdates = true; - const delta = osparc.wrapper.JsonDiffPatch.getInstance().diff(myStudy, updatedStudy); - const jsonPatches = osparc.wrapper.JsonDiffPatch.getInstance().deltaToJsonPatches(delta); - const uiPatches = []; - const workbenchPatches = []; - const studyPatches = []; - for (const jsonPatch of jsonPatches) { - if (jsonPatch.path.startsWith('/ui/')) { - uiPatches.push(jsonPatch); - } else if (jsonPatch.path.startsWith('/workbench/')) { - workbenchPatches.push(jsonPatch); - } else { - studyPatches.push(jsonPatch); - } - } - if (workbenchPatches.length > 0) { - this.getStudy().getWorkbench().updateWorkbenchFromPatches(workbenchPatches); - } - if (uiPatches.length > 0) { - this.getStudy().getUi().updateUiFromPatches(uiPatches); - } - if (studyPatches.length > 0) { - this.getStudy().updateStudyFromPatches(studyPatches); - } - - this.__blockUpdates = false; + this.__projectDocumentReceived(data); } }, this); } }, + __projectDocumentReceived: function(data) { + const documentVersion = data["version"]; + + // Ignore outdated updates + if (this.__lastSyncedProjectVersion && documentVersion <= this.__lastSyncedProjectVersion) { + // ignore old updates + console.debug("ProjectDocument Discarded: Ignoring old", data); + return; + } + + // Always keep the latest version in pending buffer + if (!this.__pendingProjectData || documentVersion > (this.__pendingProjectData.version || 0)) { + this.__pendingProjectData = data; + } + + // Reset the timer if it's already running + if (this.__applyProjectDocumentTimer) { + console.debug("ProjectDocument Discarded: Resetting applyProjectDocument timer"); + clearTimeout(this.__applyProjectDocumentTimer); + } + + // Throttle applying updates + this.__applyProjectDocumentTimer = setTimeout(() => { + if (!this.__pendingProjectData) { + return; + } + this.__applyProjectDocumentTimer = null; + + // Apply the latest buffered project document + const latestData = this.__pendingProjectData; + this.__pendingProjectData = null; + + this.__applyProjectDocument(latestData); + }, 3*this.self().THROTTLE_PATCH_TIME); + // make it 3 times longer. + // when another client adds a node: + // - there is a POST call + // - then (after the throttle) a PATCH on its position + // without waiting for it 3 times, this client might place it on the default 0,0 + }, + + __applyProjectDocument: function(data) { + console.debug("ProjectDocument applying:", data); + this.__lastSyncedProjectVersion = data["version"]; + const updatedProjectDocument = data["document"]; + + // curate projectDocument:updated document + this.self().curateBackendProjectDocument(updatedProjectDocument); + + const myStudy = this.getStudy().serialize(); + // curate myStudy + this.self().curateFrontendProjectDocument(myStudy); + + this.__blockUpdates = true; + const delta = osparc.wrapper.JsonDiffPatch.getInstance().diff(myStudy, updatedProjectDocument); + const jsonPatches = osparc.wrapper.JsonDiffPatch.getInstance().deltaToJsonPatches(delta); + const uiPatches = []; + const workbenchPatches = []; + const studyPatches = []; + for (const jsonPatch of jsonPatches) { + if (jsonPatch.path.startsWith('/ui/')) { + uiPatches.push(jsonPatch); + } else if (jsonPatch.path.startsWith('/workbench/')) { + workbenchPatches.push(jsonPatch); + } else { + studyPatches.push(jsonPatch); + } + } + if (workbenchPatches.length > 0) { + this.getStudy().getWorkbench().updateWorkbenchFromPatches(workbenchPatches, uiPatches); + } + if (uiPatches.length > 0) { + this.getStudy().getUi().updateUiFromPatches(uiPatches); + } + if (studyPatches.length > 0) { + this.getStudy().updateStudyFromPatches(studyPatches); + } + + this.__blockUpdates = false; + }, + __listenToLogger: function() { const socket = osparc.wrapper.WebSocket.getInstance(); 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 29c45220dba4..c836eba7bac8 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -246,9 +246,13 @@ qx.Class.define("osparc.desktop.WorkbenchView", { this.__connectEvents(); study.getWorkbench().addListener("pipelineChanged", () => this.__evalSlidesButtons()); + study.getWorkbench().addListener("nodeAdded", e => { + const node = e.getData(); + this.__nodeAdded(node); + }); study.getWorkbench().addListener("nodeRemoved", e => { const {nodeId, connectedEdgeIds} = e.getData(); - this.nodeRemoved(nodeId, connectedEdgeIds); + this.__nodeRemoved(nodeId, connectedEdgeIds); }); study.getUi().getSlideshow().addListener("changeSlideshow", () => this.__evalSlidesButtons()); study.getUi().addListener("changeMode", () => this.__evalSlidesButtons()); @@ -798,6 +802,12 @@ qx.Class.define("osparc.desktop.WorkbenchView", { } else if (node) { this.__populateSecondaryColumnNode(node); } + + if (node instanceof osparc.data.model.Node) { + node.getStudy().bind("pipelineRunning", this.__serviceOptionsPage, "enabled", { + converter: pipelineRunning => !pipelineRunning + }); + } }, __populateSecondaryColumnStudy: function(study) { @@ -1039,7 +1049,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { this.__serviceOptionsPage.bind("width", vBox, "width"); // HEADER - const nodeMetadata = node.getMetaData(); + const nodeMetadata = node.getMetadata(); const version = osparc.store.Services.getVersionDisplay(nodeMetadata["key"], nodeMetadata["version"]); const header = new qx.ui.basic.Label(`${nodeMetadata["name"]} ${version}`).set({ paddingLeft: 5 @@ -1127,6 +1137,19 @@ qx.Class.define("osparc.desktop.WorkbenchView", { this.addListener("disappear", () => qx.event.message.Bus.getInstance().unsubscribe("maximizeIframe", maximizeIframeCb, this), this); }, + __nodeAdded: function(node) { + this.__workbenchUI.addNode(node, node.getPosition()); + }, + + __nodeRemoved: function(nodeId, connectedEdgeIds) { + // remove first the connected edges + connectedEdgeIds.forEach(edgeId => { + this.__workbenchUI.clearEdge(edgeId); + }); + // then remove the node + this.__workbenchUI.clearNode(nodeId); + }, + __removeNode: function(nodeId) { const workbench = this.getStudy().getWorkbench(); const node = workbench.getNode(nodeId); @@ -1182,15 +1205,6 @@ qx.Class.define("osparc.desktop.WorkbenchView", { } }, - nodeRemoved: function(nodeId, connectedEdgeIds) { - // remove first the connected edges - connectedEdgeIds.forEach(edgeId => { - this.__workbenchUI.clearEdge(edgeId); - }); - // then remove the node - this.__workbenchUI.clearNode(nodeId); - }, - __removeEdge: function(edgeId) { const workbench = this.getStudy().getWorkbench(); const removed = workbench.removeEdge(edgeId); diff --git a/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js b/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js index c3a89731b081..25412ac55419 100644 --- a/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js +++ b/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js @@ -908,6 +908,7 @@ qx.Class.define("osparc.form.renderer.PropForm", { if (!this.__isPortAvailable(toPortId)) { return false; } + const ctrlLink = this.getControlLink(toPortId); ctrlLink.setEnabled(false); this._form.getControl(toPortId)["link"] = { @@ -926,21 +927,28 @@ qx.Class.define("osparc.form.renderer.PropForm", { ctrlLink.addListener("mouseover", () => highlightEdgeUI(true)); ctrlLink.addListener("mouseout", () => highlightEdgeUI(false)); - const workbench = study.getWorkbench(); - const fromNode = workbench.getNode(fromNodeId); - const port = fromNode.getOutput(fromPortId); - const fromPortLabel = port ? port.label : null; - fromNode.bind("label", ctrlLink, "value", { - converter: label => label + ": " + fromPortLabel - }); - // Hack: Show tooltip if element is disabled - const addToolTip = () => { - ctrlLink.getContentElement().removeAttribute("title"); - const toolTipText = fromNode.getLabel() + ":\n" + fromPortLabel; - ctrlLink.getContentElement().setAttribute("title", toolTipText); - }; - fromNode.addListener("changeLabel", () => addToolTip()); - addToolTip(); + const fromNode = study.getWorkbench().getNode(fromNodeId); + const prettifyLinkString = () => { + const port = fromNode.getOutput(fromPortId); + const fromPortLabel = port ? port.label : null; + fromNode.bind("label", ctrlLink, "value", { + converter: label => label + ": " + fromPortLabel + }); + + // Hack: Show tooltip if element is disabled + const addToolTip = () => { + ctrlLink.getContentElement().removeAttribute("title"); + const toolTipText = fromNode.getLabel() + ":\n" + fromPortLabel; + ctrlLink.getContentElement().setAttribute("title", toolTipText); + }; + fromNode.addListener("changeLabel", () => addToolTip()); + addToolTip(); + } + if (fromNode.getMetadata()) { + prettifyLinkString(); + } else { + fromNode.addListenerOnce("changeMetadata", () => prettifyLinkString(), this); + } this.__portLinkAdded(toPortId, fromNodeId, fromPortId); diff --git a/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js b/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js index dce44818824e..ac7a1a3bd4de 100644 --- a/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js +++ b/services/static-webserver/client/source/class/osparc/form/renderer/PropFormBase.js @@ -225,7 +225,7 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { const changedXUnits = this.getChangedXUnits(); Object.keys(changedXUnits).forEach(portId => { const ctrl = this._form.getControl(portId); - const nodeMD = this.getNode().getMetaData(); + const nodeMD = this.getNode().getMetadata(); const { unitPrefix } = osparc.utils.Units.decomposeXUnit(nodeMD.inputs[portId]["x_unit"]); @@ -273,7 +273,7 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { const ctrl = this._form.getControl(portId); xUnits[portId] = osparc.utils.Units.composeXUnit(ctrl.unit, ctrl.unitPrefix); } - const nodeMD = this.getNode().getMetaData(); + const nodeMD = this.getNode().getMetadata(); const changedXUnits = {}; for (const portId in xUnits) { if (xUnits[portId] === null) { @@ -350,7 +350,7 @@ qx.Class.define("osparc.form.renderer.PropFormBase", { if (unit && unitRegistered) { unitLabel.addListener("pointerover", () => unitLabel.setCursor("pointer"), this); unitLabel.addListener("pointerout", () => unitLabel.resetCursor(), this); - const nodeMD = this.getNode().getMetaData(); + const nodeMD = this.getNode().getMetadata(); const originalUnit = "x_unit" in nodeMD.inputs[item.key] ? osparc.utils.Units.decomposeXUnit(nodeMD.inputs[item.key]["x_unit"]) : null; unitLabel.addListener("tap", () => { const nextPrefix = osparc.utils.Units.getNextPrefix(item.unitPrefix, originalUnit.unitPrefix); diff --git a/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js b/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js index 8f3685a6c4c9..54b44fa0cc8c 100644 --- a/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js +++ b/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js @@ -106,15 +106,17 @@ qx.Class.define("osparc.info.ServiceUtils", { wrap: true, maxWidth: 220, }); - authors.set({ - value: serviceData["authors"].map(author => author["name"]).join(", "), - }); - serviceData["authors"].forEach(author => { - const oldTTT = authors.getToolTipText(); + if (serviceData["authors"]) { authors.set({ - toolTipText: (oldTTT ? oldTTT : "") + `${author["email"]} - ${author["affiliation"]}
` + value: serviceData["authors"].map(author => author["name"]).join(", "), }); - }); + serviceData["authors"].forEach(author => { + const oldTTT = authors.getToolTipText(); + authors.set({ + toolTipText: (oldTTT ? oldTTT : "") + `${author["email"]} - ${author["affiliation"]}
` + }); + }); + } return authors; }, @@ -122,20 +124,27 @@ qx.Class.define("osparc.info.ServiceUtils", { * @param serviceData {Object} Serialized Service Object */ createAccessRights: function(serviceData) { - let permissions = ""; - const myGID = osparc.auth.Data.getInstance().getGroupId(); - const ar = serviceData["accessRights"]; - if (myGID in ar) { - if (ar[myGID]["write"]) { - permissions = qx.locale.Manager.tr("Write"); - } else if (ar[myGID]["execute"]) { - permissions = qx.locale.Manager.tr("Execute"); + const allMyGIds = osparc.store.Groups.getInstance().getAllMyGroupIds(); + const accessRights = serviceData["accessRights"]; + const permissions = new Set(); + allMyGIds.forEach(gId => { + if (gId in accessRights) { + if (accessRights[gId]["write"]) { + permissions.add("write"); + } else if (accessRights[gId]["execute"]) { + permissions.add("read"); + } } + }); + const accessRightsLabel = new qx.ui.basic.Label(); + if (permissions.has("write")) { + accessRightsLabel.setValue(osparc.data.Roles.SERVICES["write"].label); + } else if (permissions.has("read")) { + accessRightsLabel.setValue(osparc.data.Roles.SERVICES["read"].label); } else { - permissions = qx.locale.Manager.tr("Public"); + accessRightsLabel.setValue(qx.locale.Manager.tr("Public")); } - const accessRights = new qx.ui.basic.Label(permissions); - return accessRights; + return accessRightsLabel; }, /** diff --git a/services/static-webserver/client/source/class/osparc/info/StudyUtils.js b/services/static-webserver/client/source/class/osparc/info/StudyUtils.js index 3ea10b1efe9e..16b8f39af2dc 100644 --- a/services/static-webserver/client/source/class/osparc/info/StudyUtils.js +++ b/services/static-webserver/client/source/class/osparc/info/StudyUtils.js @@ -84,21 +84,31 @@ qx.Class.define("osparc.info.StudyUtils", { * @param study {osparc.data.model.Study} Study Model */ createAccessRights: function(study) { - const accessRights = new qx.ui.basic.Label(); - let permissions = ""; - const myGID = osparc.auth.Data.getInstance().getGroupId(); - const ar = study.getAccessRights(); - if (myGID in ar) { - if (ar[myGID]["delete"]) { - permissions = qx.locale.Manager.tr("Owner"); - } else if (ar[myGID]["write"]) { - permissions = qx.locale.Manager.tr("Editor"); - } else if (ar[myGID]["read"]) { - permissions = qx.locale.Manager.tr("User"); + const allMyGIds = osparc.store.Groups.getInstance().getAllMyGroupIds(); + const accessRights = study.getAccessRights(); + const permissions = new Set(); + allMyGIds.forEach(gId => { + if (gId in accessRights) { + if (accessRights[gId]["delete"]) { + permissions.add("delete"); + } else if (accessRights[gId]["write"]) { + permissions.add("write"); + } else if (accessRights[gId]["read"]) { + permissions.add("read"); + } } + }); + const accessRightsLabel = new qx.ui.basic.Label(); + if (permissions.has("delete")) { + accessRightsLabel.setValue(osparc.data.Roles.STUDY["delete"].label); + } else if (permissions.has("write")) { + accessRightsLabel.setValue(osparc.data.Roles.STUDY["write"].label); + } else if (permissions.has("read")) { + accessRightsLabel.setValue(osparc.data.Roles.STUDY["read"].label); + } else { + accessRightsLabel.setValue(qx.locale.Manager.tr("Public")); } - accessRights.setValue(permissions); - return accessRights; + return accessRightsLabel; }, /** diff --git a/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js b/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js index 762f71343519..8725dddfce69 100644 --- a/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js +++ b/services/static-webserver/client/source/class/osparc/node/BootOptionsView.js @@ -38,7 +38,7 @@ qx.Class.define("osparc.node.BootOptionsView", { const buttonsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); - const nodeMetadata = node.getMetaData(); + const nodeMetadata = node.getMetadata(); const workbenchData = node.getWorkbench().serialize(); const nodeId = node.getNodeId(); const bootModeSB = osparc.data.model.Node.getBootModesSelectBox(nodeMetadata, workbenchData, nodeId); diff --git a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js index 23a390106f4c..d67382c725da 100644 --- a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js +++ b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js @@ -65,7 +65,7 @@ qx.Class.define("osparc.node.LifeCycleView", { const node = this.getNode(); if (node.isDeprecated()) { - const deprecateDateLabel = new qx.ui.basic.Label(osparc.service.Utils.getDeprecationDateText(node.getMetaData())).set({ + const deprecateDateLabel = new qx.ui.basic.Label(osparc.service.Utils.getDeprecationDateText(node.getMetadata())).set({ rich: true }); this._add(deprecateDateLabel); diff --git a/services/static-webserver/client/source/class/osparc/node/ParameterEditor.js b/services/static-webserver/client/source/class/osparc/node/ParameterEditor.js index a762a75af38a..b1993171f0c9 100644 --- a/services/static-webserver/client/source/class/osparc/node/ParameterEditor.js +++ b/services/static-webserver/client/source/class/osparc/node/ParameterEditor.js @@ -30,7 +30,7 @@ qx.Class.define("osparc.node.ParameterEditor", { statics: { getParameterOutputType: function(node) { - const metadata = node.getMetaData(); + const metadata = node.getMetadata(); return osparc.service.Utils.getParameterType(metadata); }, diff --git a/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js b/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js index 86b9b713f188..26250a85f8dd 100644 --- a/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js +++ b/services/static-webserver/client/source/class/osparc/node/slideshow/BaseNodeView.js @@ -217,7 +217,7 @@ qx.Class.define("osparc.node.slideshow.BaseNodeView", { __openServiceDetails: function() { const node = this.getNode(); - const metadata = node.getMetaData(); + const metadata = node.getMetadata(); const serviceDetails = new osparc.info.ServiceLarge(metadata, { nodeId: node.getNodeId(), label: node.getLabel(), @@ -277,13 +277,6 @@ qx.Class.define("osparc.node.slideshow.BaseNodeView", { return this._settingsLayout; }, - /** - * @abstract - */ - isSettingsGroupShowable: function() { - throw new Error("Abstract method called!"); - }, - /** * @abstract */ diff --git a/services/static-webserver/client/source/class/osparc/node/slideshow/FilePickerView.js b/services/static-webserver/client/source/class/osparc/node/slideshow/FilePickerView.js index cd66e03d48aa..82f3c0480f72 100644 --- a/services/static-webserver/client/source/class/osparc/node/slideshow/FilePickerView.js +++ b/services/static-webserver/client/source/class/osparc/node/slideshow/FilePickerView.js @@ -27,11 +27,6 @@ qx.Class.define("osparc.node.slideshow.FilePickerView", { }, members: { - // overridden - isSettingsGroupShowable: function() { - return false; - }, - // overridden _addSettings: function() { return; 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 1d74811ad0cf..f702aa747bf0 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 @@ -38,13 +38,6 @@ qx.Class.define("osparc.node.slideshow.NodeView", { statics: { LOGGER_HEIGHT: 28, - - isPropsFormShowable: function(node) { - if (node && ("getPropsForm" in node) && node.getPropsForm()) { - return node.getPropsForm().hasVisibleInputs(); - } - return false; - }, }, members: { @@ -55,11 +48,22 @@ qx.Class.define("osparc.node.slideshow.NodeView", { this._settingsLayout.removeAll(); const node = this.getNode(); - const propsForm = node.getPropsForm(); - if (propsForm && node.hasInputs()) { - this._settingsLayout.add(propsForm); + if ( + node.isComputational() && + node.hasInputs() && + "getPropsForm" in node && + node.getPropsForm() && + node.getPropsForm().hasVisibleInputs() + ) { + this._settingsLayout.add(node.getPropsForm()); } - this.__checkSettingsVisibility(); + + const showSettings = node.isComputational(); + this._settingsLayout.setVisibility(showSettings ? "visible" : "excluded"); + + node.getStudy().bind("pipelineRunning", this._settingsLayout, "enabled", { + converter: pipelineRunning => !pipelineRunning + }); this._mainView.add(this._settingsLayout); }, @@ -128,19 +132,6 @@ qx.Class.define("osparc.node.slideshow.NodeView", { this.base(arguments, node); }, - __checkSettingsVisibility: function() { - const isSettingsGroupShowable = this.isSettingsGroupShowable(); - this._settingsLayout.setVisibility(isSettingsGroupShowable ? "visible" : "excluded"); - }, - - isSettingsGroupShowable: function() { - const node = this.getNode(); - if (node.isComputational()) { - return this.self().isPropsFormShowable(node); - } - return false; - }, - __iFrameChanged: function() { this._iFrameLayout.removeAll(); diff --git a/services/static-webserver/client/source/class/osparc/share/AddCollaborators.js b/services/static-webserver/client/source/class/osparc/share/AddCollaborators.js index 1099aa6c1cbd..9dc2a7192c56 100644 --- a/services/static-webserver/client/source/class/osparc/share/AddCollaborators.js +++ b/services/static-webserver/client/source/class/osparc/share/AddCollaborators.js @@ -16,7 +16,7 @@ ************************************************************************ */ /** - * Widget that offers the "Share with..." button to add collaborators to a resource. + * Widget that offers the "Share" button to add collaborators to a resource. * It also provides the "Check Organization..." direct access. * As output, once the user select n gid in the NewCollaboratorsManager pop up window, * an event is fired with the list of collaborators. @@ -60,7 +60,9 @@ qx.Class.define("osparc.share.AddCollaborators", { this._add(control); break; case "share-with": - control = new qx.ui.form.Button(this.tr("Share with...")).set({ + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/share-alt/12", + label: this.tr("Share"), appearance: "form-button", alignX: "left", allowGrowX: false diff --git a/services/static-webserver/client/source/class/osparc/share/Collaborators.js b/services/static-webserver/client/source/class/osparc/share/Collaborators.js index 63509d88871b..3a15baabd6c4 100644 --- a/services/static-webserver/client/source/class/osparc/share/Collaborators.js +++ b/services/static-webserver/client/source/class/osparc/share/Collaborators.js @@ -286,10 +286,13 @@ qx.Class.define("osparc.share.Collaborators", { __createCollaboratorsListSection: function() { const vBox = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)); - const header = new qx.ui.container.Composite(new qx.ui.layout.HBox()); + const header = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); - const label = new qx.ui.basic.Label(this.tr("Shared with")); - label.set({allowGrowX: true}); + const label = new qx.ui.basic.Label(this.tr("Shared with:")); + label.set({ + allowGrowX: true, + alignY: "middle", + }); header.add(label, { flex: 1 }); @@ -306,7 +309,8 @@ qx.Class.define("osparc.share.Collaborators", { decorator: "no-border", spacing: 3, width: 150, - padding: 0 + padding: 0, + backgroundColor: "transparent", }); const collaboratorsModel = this.__collaboratorsModel = new qx.data.Array(); diff --git a/services/static-webserver/client/source/class/osparc/share/ShareTemplateWith.js b/services/static-webserver/client/source/class/osparc/share/ShareTemplateWith.js index 5a26282b57d8..9c3ebe915e64 100644 --- a/services/static-webserver/client/source/class/osparc/share/ShareTemplateWith.js +++ b/services/static-webserver/client/source/class/osparc/share/ShareTemplateWith.js @@ -53,7 +53,10 @@ qx.Class.define("osparc.share.ShareTemplateWith", { value: this.tr("Make the template accessible to:"), font: "text-14", }); - addCollaborators.getChildControl("share-with").setLabel(this.tr("Share with...")); + addCollaborators.getChildControl("share-with").set({ + icon: "@FontAwesome5Solid/share-alt/12", + label: this.tr("Share"), + }); this._add(addCollaborators); this._add(this.__selectedCollabs); diff --git a/services/static-webserver/client/source/class/osparc/store/Groups.js b/services/static-webserver/client/source/class/osparc/store/Groups.js index e367f0e9cbb3..2aa6794a6871 100644 --- a/services/static-webserver/client/source/class/osparc/store/Groups.js +++ b/services/static-webserver/client/source/class/osparc/store/Groups.js @@ -158,6 +158,15 @@ qx.Class.define("osparc.store.Groups", { return Object.keys(this.getOrganizations()); }, + getAllMyGroupIds: function() { + return [ + this.getMyGroupId(), + ...this.getOrganizationIds().map(gId => parseInt(gId)), + this.getEveryoneProductGroup().getGroupId(), + this.getEveryoneGroup().getGroupId(), + ] + }, + getGroup: function(groupId) { const groups = []; diff --git a/services/static-webserver/client/source/class/osparc/store/Services.js b/services/static-webserver/client/source/class/osparc/store/Services.js index 676eb86cb80d..224a88064d19 100644 --- a/services/static-webserver/client/source/class/osparc/store/Services.js +++ b/services/static-webserver/client/source/class/osparc/store/Services.js @@ -24,6 +24,8 @@ qx.Class.define("osparc.store.Services", { __studyServicesPromisesCached: {}, __pricingPlansCached: {}, + UNKNOWN_SERVICE_KEY: "simcore/services/frontend/unknown", + getServicesLatest: function(useCache = true) { return new Promise(resolve => { if (useCache && Object.keys(this.__servicesCached)) { @@ -102,56 +104,56 @@ qx.Class.define("osparc.store.Services", { }, getService: function(key, version, useCache = true) { + if (!this.__servicesPromisesCached) { + this.__servicesPromisesCached = {}; + } + if (!(key in this.__servicesPromisesCached)) { + this.__servicesPromisesCached[key] = {}; + } + // avoid request deduplication - if (key in this.__servicesPromisesCached && version in this.__servicesPromisesCached[key]) { + if (this.__servicesPromisesCached[key][version]) { return this.__servicesPromisesCached[key][version]; } - // Create a new promise - const promise = new Promise((resolve, reject) => { - if ( - useCache && - this.__isInCache(key, version) && - ( - this.__servicesCached[key][version] === null || - "history" in this.__servicesCached[key][version] - ) - ) { - resolve(this.__servicesCached[key][version]); - return; - } + if ( + useCache && + this.__isInCache(key, version) && + ( + this.__servicesCached[key][version] === null || + "history" in this.__servicesCached[key][version] + ) + ) { + return Promise.resolve(this.__servicesCached[key][version]); + } - if (!(key in this.__servicesPromisesCached)) { - this.__servicesPromisesCached[key] = {}; - } - const params = { - url: osparc.data.Resources.getServiceUrl(key, version) - }; - this.__servicesPromisesCached[key][version] = osparc.data.Resources.fetch("services", "getOne", params) - .then(service => { - this.__addServiceToCache(service); - // Resolve the promise locally before deleting it - resolve(service); - }) - .catch(err => { - // Store null in cache to avoid repeated failed requests - this.__addToCache(key, version, null); - console.error(err); - reject(err); - }) - .finally(() => { - // Remove the promise from the cache - delete this.__servicesPromisesCached[key][version]; - }); - }); + const params = { + url: osparc.data.Resources.getServiceUrl(key, version) + }; + const fetchPromise = osparc.data.Resources.fetch("services", "getOne", params) + .then(service => { + this.__addServiceToCache(service); + // Resolve the promise locally before deleting it + return service; + }) + .catch(err => { + // Store null in cache to avoid repeated failed requests + this.__addToCache(key, version, null); + console.error(err); + throw err; + }) + .finally(() => { + // Remove the promise from the cache + delete this.__servicesPromisesCached[key][version]; + }); // Store the promise in the cache // The point of keeping this assignment outside of the main Promise block is to // ensure that the promise is immediately stored in the cache before any asynchronous // operations (like fetch) are executed. This prevents duplicate requests for the // same key and version when multiple consumers call getService concurrently. - this.__servicesPromisesCached[key][version] = promise; - return promise; + this.__servicesPromisesCached[key][version] = fetchPromise; + return fetchPromise; }, getStudyServices: function(studyId) { @@ -400,6 +402,37 @@ qx.Class.define("osparc.store.Services", { return this.getLatest("simcore/services/frontend/iterator-consumer/probe/"+type); }, + getUnknownServiceMetadata: function() { + const key = this.UNKNOWN_SERVICE_KEY; + const version = "0.0.0"; + const versionDisplay = "Unknown"; + const releaseInfo = { + version, + versionDisplay, + retired: null, + released: "2025-08-07T11:00:00.000000", + compatibility: null, + }; + return { + key, + version, + versionDisplay, + description: "Unknown App", + type: "frontend", + name: "Unknown", + inputs: {}, + outputs: {}, + accessRights: { + 1: { + execute: true, + write: false, + } + }, + release: releaseInfo, + history: [releaseInfo], + }; + }, + __addServiceToCache: function(service) { this.__addHit(service); this.__addTSRInfo(service); diff --git a/services/static-webserver/client/source/class/osparc/store/Support.js b/services/static-webserver/client/source/class/osparc/store/Support.js index 028030c826dc..13c8cbd8d051 100644 --- a/services/static-webserver/client/source/class/osparc/store/Support.js +++ b/services/static-webserver/client/source/class/osparc/store/Support.js @@ -160,12 +160,15 @@ qx.Class.define("osparc.store.Support", { }, getMailToLabel: function(email, subject) { - const mailto = new qx.ui.basic.Label(this.mailToLink(email, subject, false)).set({ + const mailto = new qx.ui.basic.Label().set({ font: "text-14", allowGrowX: true, // let it grow to make it easier to select selectable: true, rich: true, }); + if (email) { + mailto.setValue(this.mailToLink(email, subject, false)); + } return mailto; }, diff --git a/services/static-webserver/client/source/class/osparc/ui/basic/Thumbnail.js b/services/static-webserver/client/source/class/osparc/ui/basic/Thumbnail.js index ff7c6f5d98d2..876abfbdb1de 100644 --- a/services/static-webserver/client/source/class/osparc/ui/basic/Thumbnail.js +++ b/services/static-webserver/client/source/class/osparc/ui/basic/Thumbnail.js @@ -83,7 +83,7 @@ qx.Class.define("osparc.ui.basic.Thumbnail", { __applySource: function(val) { const image = this.getChildControl("image"); if (val) { - if (osparc.utils.Utils.isValidHttpUrl(val)) { + if (!val.startsWith("osparc/") && osparc.utils.Utils.isValidHttpUrl(val)) { osparc.utils.Utils.setUrlSourceToImage(image, val); } else { image.setSource(val); diff --git a/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js b/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js index 89f0d7c87b7c..0f9130d755a4 100644 --- a/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js +++ b/services/static-webserver/client/source/class/osparc/ui/list/ListItem.js @@ -60,6 +60,7 @@ qx.Class.define("osparc.ui.list.ListItem", { padding: 5, minHeight: 48, alignY: "middle", + decorator: "rounded", }); this.addListener("pointerover", this._onPointerOver, this); diff --git a/services/static-webserver/client/source/class/osparc/widget/NodeOutputs.js b/services/static-webserver/client/source/class/osparc/widget/NodeOutputs.js index c8da28a0c48e..7410c6c547ea 100644 --- a/services/static-webserver/client/source/class/osparc/widget/NodeOutputs.js +++ b/services/static-webserver/client/source/class/osparc/widget/NodeOutputs.js @@ -46,7 +46,7 @@ qx.Class.define("osparc.widget.NodeOutputs", { this.set({ node, - ports: node.getMetaData().outputs + ports: node.getMetadata().outputs }); node.addListener("changeOutputs", () => this.__outputsChanged(), this); @@ -198,18 +198,21 @@ qx.Class.define("osparc.widget.NodeOutputs", { valueWidget = new osparc.ui.basic.LinkLabel(); if ("store" in value) { // it's a file - const download = true; - const locationId = value.store; - const fileId = value.path; const filename = value.filename || osparc.file.FilePicker.getFilenameFromPath(value); valueWidget.setValue(filename); valueWidget.eTag = value["eTag"]; - osparc.store.Data.getInstance().getPresignedLink(download, locationId, fileId) - .then(presignedLinkData => { - if ("resp" in presignedLinkData && presignedLinkData.resp) { - valueWidget.setUrl(presignedLinkData.resp.link); - } - }); + const download = true; + const locationId = value.store; + const fileId = value.path; + // request the presigned link only when the widget is shown + valueWidget.addListenerOnce("appear", () => { + osparc.store.Data.getInstance().getPresignedLink(download, locationId, fileId) + .then(presignedLinkData => { + if ("resp" in presignedLinkData && presignedLinkData.resp) { + valueWidget.setUrl(presignedLinkData.resp.link); + } + }); + }); } else if ("downloadLink" in value) { // it's a link const filename = (value.filename && value.filename.length > 0) ? value.filename : osparc.file.FileDownloadLink.extractLabelFromLink(value["downloadLink"]); diff --git a/services/static-webserver/client/source/class/osparc/widget/NodesTree.js b/services/static-webserver/client/source/class/osparc/widget/NodesTree.js index e6e9fb599c67..309389183865 100644 --- a/services/static-webserver/client/source/class/osparc/widget/NodesTree.js +++ b/services/static-webserver/client/source/class/osparc/widget/NodesTree.js @@ -74,34 +74,40 @@ qx.Class.define("osparc.widget.NodesTree", { statics: { __getSortingValue: function(node) { - if (node.isFilePicker()) { - return osparc.service.Utils.getSorting("file"); - } else if (node.isParameter()) { - return osparc.service.Utils.getSorting("parameter"); - } else if (node.isIterator()) { - return osparc.service.Utils.getSorting("iterator"); - } else if (node.isProbe()) { - return osparc.service.Utils.getSorting("probe"); + if (node.getMetadata()) { + if (node.isFilePicker()) { + return osparc.service.Utils.getSorting("file"); + } else if (node.isParameter()) { + return osparc.service.Utils.getSorting("parameter"); + } else if (node.isIterator()) { + return osparc.service.Utils.getSorting("iterator"); + } else if (node.isProbe()) { + return osparc.service.Utils.getSorting("probe"); + } + return osparc.service.Utils.getSorting(node.getMetadata().type); } - return osparc.service.Utils.getSorting(node.getMetaData().type); + return null; }, __getIcon: function(node) { let icon = null; - if (node.isFilePicker()) { - icon = osparc.service.Utils.getIcon("file"); - } else if (node.isParameter()) { - icon = osparc.service.Utils.getIcon("parameter"); - } else if (node.isIterator()) { - icon = osparc.service.Utils.getIcon("iterator"); - } else if (node.isProbe()) { - icon = osparc.service.Utils.getIcon("probe"); - } else { - icon = osparc.service.Utils.getIcon(node.getMetaData().type); - } - if (icon) { - icon += "14"; + if (node.getMetadata()) { + if (node.isFilePicker()) { + icon = osparc.service.Utils.getIcon("file"); + } else if (node.isParameter()) { + icon = osparc.service.Utils.getIcon("parameter"); + } else if (node.isIterator()) { + icon = osparc.service.Utils.getIcon("iterator"); + } else if (node.isProbe()) { + icon = osparc.service.Utils.getIcon("probe"); + } else { + icon = osparc.service.Utils.getIcon(node.getMetadata().type); + } + if (icon) { + icon += "14"; + } } + return icon; }, @@ -125,22 +131,32 @@ qx.Class.define("osparc.widget.NodesTree", { id: node.getNodeId(), label: "Node", children: [], - icon: this.__getIcon(node), + icon: "", iconColor: "text", sortingValue: this.__getSortingValue(node), node }; + const nodeModel = qx.data.marshal.Json.createModel(nodeData, true); node.bind("label", nodeModel, "label"); - if (node.isDynamic()) { - node.getStatus().bind("interactive", nodeModel, "iconColor", { - converter: status => osparc.service.StatusUI.getColor(status) - }); - } else if (node.isComputational()) { - node.getStatus().bind("running", nodeModel, "iconColor", { - converter: status => osparc.service.StatusUI.getColor(status) - }); + const populateWithMetadata = () => { + if (node.isDynamic()) { + node.getStatus().bind("interactive", nodeModel, "iconColor", { + converter: status => osparc.service.StatusUI.getColor(status) + }); + } else if (node.isComputational()) { + node.getStatus().bind("running", nodeModel, "iconColor", { + converter: status => osparc.service.StatusUI.getColor(status) + }); + } + nodeModel.setIcon(this.__getIcon(node)); } + if (node.getMetadata()) { + populateWithMetadata(); + } else { + node.addListenerOnce("changeMetadata", () => populateWithMetadata(), this); + } + return nodeModel; } }, @@ -290,7 +306,7 @@ qx.Class.define("osparc.widget.NodesTree", { }); } else { const node = study.getWorkbench().getNode(nodeId); - const metadata = node.getMetaData(); + const metadata = node.getMetadata(); const serviceDetails = new osparc.info.ServiceLarge(metadata, { nodeId, label: node.getLabel(), diff --git a/services/static-webserver/client/source/class/osparc/widget/Renamer.js b/services/static-webserver/client/source/class/osparc/widget/Renamer.js index fbba9f5184e4..a5c1929b363f 100644 --- a/services/static-webserver/client/source/class/osparc/widget/Renamer.js +++ b/services/static-webserver/client/source/class/osparc/widget/Renamer.js @@ -41,7 +41,7 @@ qx.Class.define("osparc.widget.Renamer", { this.base(arguments, winTitle || this.tr("Rename")); const maxWidth = 350; - const minWidth = 150; + const minWidth = 200; const labelWidth = oldLabel ? Math.min(Math.max(parseInt(oldLabel.length*4), minWidth), maxWidth) : minWidth; this.set({ layout: new qx.ui.layout.VBox(5), diff --git a/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js b/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js index 1213ec57078c..d7005896e898 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/NodeUI.js @@ -63,7 +63,7 @@ qx.Class.define("osparc.workbench.NodeUI", { captionBar.set({ cursor: "move", paddingRight: 0, - paddingLeft: this.self().PORT_WIDTH + paddingLeft: this.self().PORT_DIAMETER - 6, }); const menuBtn = this.__getMenuButton(); @@ -105,8 +105,8 @@ qx.Class.define("osparc.workbench.NodeUI", { }, type: { - check: ["normal", "file", "parameter", "iterator", "probe"], - init: "normal", + check: ["computational", "dynamic", "file", "parameter", "iterator", "probe", "unknown"], + init: null, nullable: false, apply: "__applyType" }, @@ -133,8 +133,8 @@ qx.Class.define("osparc.workbench.NodeUI", { NODE_WIDTH: 180, NODE_HEIGHT: 80, FILE_NODE_WIDTH: 120, - PORT_HEIGHT: 18, - PORT_WIDTH: 11, + PORT_DIAMETER: 18, + PORT_MARGIN_TOP: 4, CONTENT_PADDING: 2, PORT_CONNECTED: "@FontAwesome5Regular/dot-circle/18", PORT_DISCONNECTED: "@FontAwesome5Regular/circle/18", @@ -220,13 +220,20 @@ qx.Class.define("osparc.workbench.NodeUI", { column: this.self().CAPTION_POS.DEPRECATED }); break; - case "chips": { + case "middle-container": control = new qx.ui.container.Composite(new qx.ui.layout.Flow(3, 3).set({ alignY: "middle" })).set({ - margin: [3, 4] + padding: [3, 4] }); - let nodeType = this.getNode().getMetaData().type; + this.add(control, { + row: 0, + column: 1 + }); + break; + case "node-type-chip": { + control = new osparc.ui.basic.Chip(); + let nodeType = this.getNode().getMetadata().type; if (this.getNode().isIterator()) { nodeType = "iterator"; } else if (this.getNode().isProbe()) { @@ -234,32 +241,39 @@ qx.Class.define("osparc.workbench.NodeUI", { } const type = osparc.service.Utils.getType(nodeType); if (type) { - const chip = new osparc.ui.basic.Chip().set({ + control.set({ icon: type.icon + "14", toolTipText: type.label }); - control.add(chip); + } else if (this.getNode().isUnknown()) { + control.set({ + icon: "@FontAwesome5Solid/question/14", + toolTipText: "Unknown", + }); } - const nodeStatus = new osparc.ui.basic.NodeStatusUI(this.getNode()); - control.add(nodeStatus); - const statusLabel = nodeStatus.getChildControl("label"); + this.getChildControl("middle-container").add(control); + break; + } + case "node-status-ui": { + control = new osparc.ui.basic.NodeStatusUI(this.getNode()).set({ + maxHeight: 20, + font: "text-10", + }); + const statusLabel = control.getChildControl("label"); const requestOpenLogger = () => this.fireEvent("requestOpenLogger"); const evaluateLabel = () => { const failed = statusLabel.getValue() === "Unsuccessful"; statusLabel.setCursor(failed ? "pointer" : "auto"); - if (nodeStatus.hasListener("tap")) { - nodeStatus.removeListener("tap", requestOpenLogger); + if (control.hasListener("tap")) { + control.removeListener("tap", requestOpenLogger); } if (failed) { - nodeStatus.addListener("tap", requestOpenLogger); + control.addListener("tap", requestOpenLogger); } }; evaluateLabel(); statusLabel.addListener("changeValue", evaluateLabel); - this.add(control, { - row: 0, - column: 1 - }); + this.getChildControl("middle-container").add(control); break; } case "progress": @@ -293,16 +307,19 @@ qx.Class.define("osparc.workbench.NodeUI", { }); this.resetThumbnail(); - this.__createWindowLayout(); + this.__createContentLayout(); }, - __createWindowLayout: function() { + __createContentLayout: function() { const node = this.getNode(); + if (node) { + this.getChildControl("middle-container").removeAll(); + this.getChildControl("node-type-chip"); + this.getChildControl("node-status-ui"); - this.getChildControl("chips").show(); - - if (node.isComputational() || node.isFilePicker() || node.isIterator()) { - this.getChildControl("progress").show(); + if (node.isComputational() || node.isFilePicker() || node.isIterator()) { + this.getChildControl("progress"); + } } }, @@ -313,7 +330,7 @@ qx.Class.define("osparc.workbench.NodeUI", { setTimeout(() => this.fireEvent("updateNodeDecorator"), 50); } }); - const metadata = node.getMetaData(); + const metadata = node.getMetadata(); this.__createPorts(true, Boolean((metadata && metadata.inputs && Object.keys(metadata.inputs).length))); this.__createPorts(false, Boolean((metadata && metadata.outputs && Object.keys(metadata.outputs).length))); if (node.isComputational() || node.isFilePicker()) { @@ -321,7 +338,11 @@ qx.Class.define("osparc.workbench.NodeUI", { converter: val => val === null ? 0 : val }); } - if (node.isFilePicker()) { + if (node.isComputational()) { + this.setType("computational"); + } else if (node.isDynamic()) { + this.setType("dynamic"); + } else if (node.isFilePicker()) { this.setType("file"); } else if (node.isParameter()) { this.setType("parameter"); @@ -330,6 +351,8 @@ qx.Class.define("osparc.workbench.NodeUI", { this.setType("iterator"); } else if (node.isProbe()) { this.setType("probe"); + } else if (node.isUnknown()) { + this.setType("unknown"); } this.addListener("resize", () => { setTimeout(() => this.fireEvent("updateNodeDecorator"), 50); @@ -375,11 +398,10 @@ qx.Class.define("osparc.workbench.NodeUI", { } const lock = this.getChildControl("lock"); - if (node.getPropsForm()) { - node.getPropsForm().bind("enabled", lock, "visibility", { - converter: val => val ? "excluded" : "visible" - }); - } + node.getStudy().bind("pipelineRunning", lock, "visibility", { + converter: pipelineRunning => pipelineRunning ? "visible" : "excluded" + }); + this.__markerBtn.show(); this.getNode().bind("marker", this.__markerBtn, "label", { converter: val => val ? this.tr("Remove Marker") : this.tr("Add Marker") @@ -411,7 +433,7 @@ qx.Class.define("osparc.workbench.NodeUI", { textColor: osparc.service.StatusUI.getColor("deprecated") }); let ttMsg = osparc.service.Utils.DEPRECATED_SERVICE_TEXT; - const deprecatedDateMsg = osparc.service.Utils.getDeprecationDateText(node.getMetaData()); + const deprecatedDateMsg = osparc.service.Utils.getDeprecationDateText(node.getMetadata()); if (deprecatedDateMsg) { ttMsg = ttMsg + "
" + deprecatedDateMsg; } @@ -468,6 +490,9 @@ qx.Class.define("osparc.workbench.NodeUI", { case "probe": this.__turnIntoProbeUI(); break; + case "unknown": + this.__turnIntoUnknownUI(); + break; } }, @@ -493,8 +518,8 @@ qx.Class.define("osparc.workbench.NodeUI", { const width = this.self().FILE_NODE_WIDTH; this.__setNodeUIWidth(width); - const chipContainer = this.getChildControl("chips"); - chipContainer.exclude(); + const middleContainer = this.getChildControl("middle-container"); + middleContainer.exclude(); if (this.hasChildControl("progress")) { this.getChildControl("progress").exclude(); @@ -527,8 +552,8 @@ qx.Class.define("osparc.workbench.NodeUI", { const label = new qx.ui.basic.Label().set({ font: "text-18" }); - const chipContainer = this.getChildControl("chips"); - chipContainer.add(label); + const middleContainer = this.getChildControl("middle-container"); + middleContainer.add(label); this.getNode().bind("outputs", label, "value", { converter: outputs => { @@ -578,13 +603,22 @@ qx.Class.define("osparc.workbench.NodeUI", { paddingLeft: 5, font: "text-12" }); - const chipContainer = this.getChildControl("chips"); - chipContainer.add(linkLabel); + const middleContainer = this.getChildControl("middle-container"); + middleContainer.add(linkLabel); this.getNode().getPropsForm().addListener("linkFieldModified", () => this.__setProbeValue(linkLabel), this); this.__setProbeValue(linkLabel); }, + __turnIntoUnknownUI: function() { + const width = 110; + this.__setNodeUIWidth(width); + + this.setEnabled(false); + + this.fireEvent("updateNodeDecorator"); + }, + __checkTurnIntoIteratorUI: function() { const outputs = this.getNode().getOutputs(); const portKey = "out_1"; @@ -631,9 +665,9 @@ qx.Class.define("osparc.workbench.NodeUI", { converter: outputs => { if (portKey in outputs && "value" in outputs[portKey] && outputs[portKey]["value"]) { const val = outputs[portKey]["value"]; - if (this.getNode().getMetaData()["key"].includes("probe/array")) { + if (this.getNode().getMetadata()["key"].includes("probe/array")) { return "[" + val.join(",") + "]"; - } else if (this.getNode().getMetaData()["key"].includes("probe/file")) { + } else if (this.getNode().getMetadata()["key"].includes("probe/file")) { const filename = val.filename || osparc.file.FilePicker.getFilenameFromPath(val); populateLinkLabel(val); return filename; @@ -815,16 +849,17 @@ qx.Class.define("osparc.workbench.NodeUI", { __createPort: function(isInput, placeholder = false) { let port = null; - const width = this.self().PORT_HEIGHT; + const width = this.self().PORT_DIAMETER; if (placeholder) { port = new qx.ui.core.Spacer(width, width); } else { port = new qx.ui.basic.Image().set({ source: this.self().PORT_DISCONNECTED, // disconnected by default height: width, + width: width, + marginTop: this.self().PORT_MARGIN_TOP, draggable: true, droppable: true, - width: width, alignY: "top", backgroundColor: "background-main" }); @@ -858,7 +893,7 @@ qx.Class.define("osparc.workbench.NodeUI", { const bounds = this.getCurrentBounds(); const captionHeight = Math.max(this.getChildControl("captionbar").getSizeHint().height, this.self().captionHeight()); const x = port.isInput ? bounds.left - 6 : bounds.left + bounds.width - 1; - const y = bounds.top + captionHeight + this.self().PORT_HEIGHT/2 + 2; + const y = bounds.top + captionHeight + this.self().PORT_DIAMETER/2 + this.self().PORT_MARGIN_TOP + 2; return [x, y]; }, diff --git a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js index a074727aaa51..6ca961dd2755 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js @@ -351,10 +351,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { let nodeUI = null; try { const node = await this.__getWorkbench().createNode(service.getKey(), service.getVersion()); - nodeUI = this._createNodeUI(node.getNodeId()); - this._addNodeUIToWorkbench(nodeUI, pos); - qx.ui.core.queue.Layout.flush(); - this.__createDragDropMechanism(nodeUI); + nodeUI = this.addNode(node, pos); } catch (err) { console.error(err); } finally { @@ -364,6 +361,20 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { return nodeUI; }, + addNode: function(node, pos) { + if (pos === undefined) { + pos = { + x: 0, + y: 0, + }; + } + const nodeUI = this._createNodeUI(node.getNodeId()); + this._addNodeUIToWorkbench(nodeUI, pos); + qx.ui.core.queue.Layout.flush(); + this.__createDragDropMechanism(nodeUI); + return nodeUI; + }, + __getNodesBounds: function() { if (this.__nodesUI.length === 0) { return null; @@ -663,12 +674,12 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { const nodeUI = new osparc.workbench.NodeUI(node); this.bind("scale", nodeUI, "scale"); node.addListener("keyChanged", () => this.__selectNode(nodeUI), this); - node.addListener("createEdge", e => { + node.addListener("edgeCreated", e => { const data = e.getData(); const { nodeId1, nodeId2 } = data; this._createEdgeBetweenNodes(nodeId1, nodeId2, false); }); - node.addListener("removeEdge", e => { + node.addListener("edgeRemoved", e => { const data = e.getData(); const { nodeId1, nodeId2 } = data; this.__removeEdgeBetweenNodes(nodeId1, nodeId2); @@ -1706,7 +1717,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { __openNodeInfo: function(nodeId) { if (nodeId) { const node = this.getStudy().getWorkbench().getNode(nodeId); - const metadata = node.getMetaData(); + const metadata = node.getMetadata(); const serviceDetails = new osparc.info.ServiceLarge(metadata, { nodeId, label: node.getLabel(),