diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index e63ae8343303..d543206295de 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -336,6 +336,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__getConversationsPage, this.__getPermissionsPage, this.__getSaveAsTemplatePage, + this.__getCreateFunctionsPage, this.__getTagsPage, this.__getQualityPage, this.__getClassifiersPage, @@ -688,7 +689,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { const id = "ServicesUpdate"; const title = this.tr("Services Updates"); - const iconSrc = "@MaterialIcons/update/22"; + const iconSrc = "@MaterialIcons/update/24"; const page = this.__servicesUpdatePage = new osparc.dashboard.resources.pages.BasePage(title, iconSrc, id); this.__addOpenButton(page); @@ -801,6 +802,29 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { return page; } return null; + }, + + __getCreateFunctionsPage: function() { + if (!osparc.utils.Resources.isStudy(this.__resourceData)) { + return null; + } + + if (!osparc.study.Utils.canCreateFunction(this.__resourceData["workbench"])) { + return null; + } + + const id = "CreateFunction"; + const iconSrc = "@MaterialIcons/functions/24"; + const title = this.tr("Create Function"); + const page = new osparc.dashboard.resources.pages.BasePage(title, iconSrc, id); + const createFunction = new osparc.study.CreateFunction(this.__resourceData); + const createFunctionButton = createFunction.getCreateFunctionButton(); + osparc.dashboard.resources.pages.BasePage.decorateHeaderButton(createFunctionButton); + const toolbar = this.self().createToolbar(); + toolbar.add(createFunctionButton); + page.addToHeader(toolbar); + page.addToContent(createFunction); + return page; } } }); diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 401bf5b7d699..f91a8a5062f4 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -586,6 +586,18 @@ qx.Class.define("osparc.data.Resources", { } } }, + /* + * FUNCTIONS + */ + "functions": { + useCache: false, + endpoints: { + getPage: { + method: "POST", + url: statics.API + "/functions" + } + } + }, /* * TASKS */ 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 b62fc32be298..9a7d4d7ad34c 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 @@ -86,7 +86,7 @@ qx.Class.define("osparc.form.renderer.PropForm", { const supportedTypes = []; const paramsMD = osparc.store.Services.getParametersMetadata(); paramsMD.forEach(paramMD => { - supportedTypes.push(osparc.node.ParameterEditor.getParameterOutputTypeFromMD(paramMD)); + supportedTypes.push(osparc.service.Utils.getParameterType(paramMD)); }); return supportedTypes.includes(field.type); }, 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 a5a6085dcaf2..a762a75af38a 100644 --- a/services/static-webserver/client/source/class/osparc/node/ParameterEditor.js +++ b/services/static-webserver/client/source/class/osparc/node/ParameterEditor.js @@ -29,17 +29,9 @@ qx.Class.define("osparc.node.ParameterEditor", { }, statics: { - getParameterOutputTypeFromMD: function(metadata) { - let type = metadata["outputs"]["out_1"]["type"]; - if (type === "ref_contentSchema") { - type = metadata["outputs"]["out_1"]["contentSchema"]["type"]; - } - return type; - }, - getParameterOutputType: function(node) { const metadata = node.getMetaData(); - return this.self().getParameterOutputTypeFromMD(metadata); + return osparc.service.Utils.getParameterType(metadata); }, setParameterOutputValue: function(node, val) { diff --git a/services/static-webserver/client/source/class/osparc/service/Utils.js b/services/static-webserver/client/source/class/osparc/service/Utils.js index ef6e687d2542..b7499816eac4 100644 --- a/services/static-webserver/client/source/class/osparc/service/Utils.js +++ b/services/static-webserver/client/source/class/osparc/service/Utils.js @@ -240,6 +240,33 @@ qx.Class.define("osparc.service.Utils", { } }); return services; - } + }, + + getParameterType: function(metadata) { + let type = metadata["outputs"]["out_1"]["type"]; + if (type === "ref_contentSchema") { + type = metadata["outputs"]["out_1"]["contentSchema"]["type"]; + } + return type; + }, + + getParameterValue: function(parameterData) { + if ( + parameterData && + parameterData["outputs"] && + parameterData["outputs"]["out_1"] + ) { + return parameterData["outputs"]["out_1"]; + } + return null; + }, + + getProbeType: function(metadata) { + let type = metadata["inputs"]["in_1"]["type"]; + if (type === "ref_contentSchema") { + type = metadata["inputs"]["in_1"]["contentSchema"]["type"]; + } + return type; + }, } }); diff --git a/services/static-webserver/client/source/class/osparc/study/CreateFunction.js b/services/static-webserver/client/source/class/osparc/study/CreateFunction.js new file mode 100644 index 000000000000..e74009b10247 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/study/CreateFunction.js @@ -0,0 +1,332 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + + +qx.Class.define("osparc.study.CreateFunction", { + extend: qx.ui.core.Widget, + + /** + * @param studyData {Object} Object containing part or the entire serialized Study Data + */ + construct: function(studyData) { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(20)); + + this.__studyData = studyData; + + this.__buildLayout(); + }, + + statics: { + typeToFunctionType: function(type) { + switch (type) { + case "number": + return "float" + case "data:*/*": + return "FileID" + } + return type; + }, + + createFunctionData: function(projectData, name, description, exposedInputs, exposedOutputs) { + const functionData = { + "project_id": projectData["uuid"], + "name": name, + "description": description, + "function_class": "project", + "input_schema": { + "schema_dict": { + "type": "object", + "properties": {} + } + }, + "output_schema": { + "schema_dict": { + "type": "object", + "properties": {} + } + }, + "default_inputs": {}, + }; + + const parameters = osparc.study.Utils.extractFunctionableParameters(projectData["workbench"]); + parameters.forEach(parameter => { + const parameterLabel = parameter["label"]; + if (exposedInputs[parameterLabel]) { + const parameterMetadata = osparc.store.Services.getMetadata(parameter["key"], parameter["version"]); + if (parameterMetadata) { + const type = osparc.service.Utils.getParameterType(parameterMetadata); + functionData["input_schema"]["schema_dict"]["properties"][parameterLabel] = { + "type": this.self().typeToFunctionType(type), + }; + } + functionData["default_inputs"][parameterLabel] = osparc.service.Utils.getParameterValue(parameter); + } + }); + + const probes = osparc.study.Utils.extractFunctionableProbes(projectData["workbench"]); + probes.forEach(probe => { + const probeLabel = probe["label"]; + if (exposedOutputs[probeLabel]) { + const probeMetadata = osparc.store.Services.getMetadata(probe["key"], probe["version"]); + if (probeMetadata) { + const type = osparc.service.Utils.getProbeType(probeMetadata); + functionData["output_schema"]["schema_dict"]["properties"][probeLabel] = { + "type": this.self().typeToFunctionType(type), + }; + } + } + }); + + return functionData; + } + }, + + members: { + __studyData: null, + __form: null, + __createFunctionBtn: null, + + __buildLayout: function() { + const form = this.__form = new qx.ui.form.Form(); + this._add(new qx.ui.form.renderer.Single(form)); + + const title = new qx.ui.form.TextField().set({ + required: true, + value: this.__studyData.name, + }); + this.addListener("appear", () => { + title.focus(); + title.activate(); + }); + form.add(title, this.tr("Name"), null, "name"); + + const description = new qx.ui.form.TextField().set({ + required: false, + }); + form.add(description, this.tr("Description"), null, "description"); + + + const exposedInputs = {}; + const exposedOutputs = {}; + + // INPUTS + const inGrid = new qx.ui.layout.Grid(10, 6); + const inputsLayout = new qx.ui.container.Composite(inGrid).set({ + allowGrowX: false, + alignX: "left", + alignY: "middle" + }); + this._add(inputsLayout); + + // header + let row = 0; + let column = 0; + const nameLabel = new qx.ui.basic.Label(this.tr("Input")); + inputsLayout.add(nameLabel, { + row, + column, + }); + column++; + const typeLabel = new qx.ui.basic.Label(this.tr("Type")); + inputsLayout.add(typeLabel, { + row, + column, + }); + column++; + const exposedLabel = new qx.ui.basic.Label(this.tr("Exposed")); + inputsLayout.add(exposedLabel, { + row, + column, + }); + column++; + const defaultValue = new qx.ui.basic.Label(this.tr("Default value")); + inputsLayout.add(defaultValue, { + row, + column, + }); + column = 0; + row++; + + const parameters = osparc.study.Utils.extractFunctionableParameters(this.__studyData["workbench"]); + parameters.forEach(parameter => { + const parameterLabel = new qx.ui.basic.Label(parameter["label"]); + inputsLayout.add(parameterLabel, { + row, + column, + }); + column++; + + const parameterMetadata = osparc.store.Services.getMetadata(parameter["key"], parameter["version"]); + if (parameterMetadata) { + const parameterType = new qx.ui.basic.Label(osparc.service.Utils.getParameterType(parameterMetadata)); + inputsLayout.add(parameterType, { + row, + column, + }); + } + column++; + + const parameterExposed = new qx.ui.form.CheckBox().set({ value: true }); + inputsLayout.add(parameterExposed, { + row, + column, + }); + exposedInputs[parameter["label"]] = true; + parameterExposed.addListener("changeValue", e => exposedInputs[parameter["label"]] = e.getData()); + column++; + + const parameterDefaultValue = new qx.ui.basic.Label(String(osparc.service.Utils.getParameterValue(parameter))); + inputsLayout.add(parameterDefaultValue, { + row, + column, + }); + column++; + + column = 0; + row++; + }); + + + // OUTPUTS + const outGrid = new qx.ui.layout.Grid(10, 6); + const outputsLayout = new qx.ui.container.Composite(outGrid).set({ + allowGrowX: false, + alignX: "left", + alignY: "middle" + }); + this._add(outputsLayout); + + // header + row = 0; + column = 0; + const nameLabel2 = new qx.ui.basic.Label(this.tr("Output")); + outputsLayout.add(nameLabel2, { + row, + column, + }); + column++; + const typeLabel2 = new qx.ui.basic.Label(this.tr("Type")); + outputsLayout.add(typeLabel2, { + row, + column, + }); + column++; + const exposedLabel2 = new qx.ui.basic.Label(this.tr("Exposed")); + outputsLayout.add(exposedLabel2, { + row, + column, + }); + column++; + column = 0; + row++; + + const probes = osparc.study.Utils.extractFunctionableProbes(this.__studyData["workbench"]); + probes.forEach(probe => { + const parameterLabel = new qx.ui.basic.Label(probe["label"]); + outputsLayout.add(parameterLabel, { + row, + column, + }); + column++; + + const probeMetadata = osparc.store.Services.getMetadata(probe["key"], probe["version"]); + if (probeMetadata) { + const probeType = new qx.ui.basic.Label(osparc.service.Utils.getProbeType(probeMetadata)); + outputsLayout.add(probeType, { + row, + column, + }); + } + column++; + + const probeExposed = new qx.ui.form.CheckBox().set({ value: true }); + outputsLayout.add(probeExposed, { + row, + column, + }); + exposedOutputs[probe["label"]] = true; + probeExposed.addListener("changeValue", e => exposedOutputs[probe["label"]] = e.getData()); + column++; + + column = 0; + row++; + }); + + const createFunctionBtn = this.__createFunctionBtn = new osparc.ui.form.FetchButton().set({ + appearance: "strong-button", + label: this.tr("Create"), + allowGrowX: false, + alignX: "right" + }); + createFunctionBtn.addListener("execute", () => { + if (this.__form.validate()) { + this.__createFunction(exposedInputs, exposedOutputs); + } + }, this); + }, + + __createFunction: function(exposedInputs, exposedOutputs) { + this.__createFunctionBtn.setFetching(true); + + // first publish it as a template + const params = { + url: { + "study_id": this.__studyData["uuid"], + "copy_data": true, + }, + }; + const options = { + pollTask: true + }; + const fetchPromise = osparc.data.Resources.fetch("studies", "postToTemplate", params, options); + const pollTasks = osparc.store.PollTasks.getInstance(); + pollTasks.createPollingTask(fetchPromise) + .then(task => { + task.addListener("resultReceived", e => { + const templateData = e.getData(); + this.__doCreateFunction(templateData, exposedInputs, exposedOutputs); + }); + }) + .catch(err => { + this.__createFunctionBtn.setFetching(false); + osparc.FlashMessenger.logError(err); + }); + }, + + __doCreateFunction: function(templateData, exposedInputs, exposedOutputs) { + const nameField = this.__form.getItem("name"); + const descriptionField = this.__form.getItem("description"); + + const functionData = this.self().createFunctionData(templateData, nameField.getValue(), descriptionField.getValue(), exposedInputs, exposedOutputs); + console.log("functionData", functionData); + + const params = { + data: functionData, + }; + osparc.data.Resources.fetch("functions", "create", params) + .then(() => osparc.FlashMessenger.logAs(this.tr("Function created"), "INFO")) + .catch(err => osparc.FlashMessenger.logError(err)) + .finally(() => this.__createFunctionBtn.setFetching(false)); + }, + + getCreateFunctionButton: function() { + return this.__createFunctionBtn; + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/study/Utils.js b/services/static-webserver/client/source/class/osparc/study/Utils.js index 8f1dad69d406..cdaa3ca712db 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -230,6 +230,53 @@ qx.Class.define("osparc.study.Utils", { return Array.from(services); }, + extractFilePickers: function(workbench) { + const parameters = Object.values(workbench).filter(srv => srv["key"].includes("simcore/services/frontend/file-picker")); + return parameters; + }, + + extractParameters: function(workbench) { + const parameters = Object.values(workbench).filter(srv => osparc.data.model.Node.isParameter(srv)); + return parameters; + }, + + extractFunctionableParameters: function(workbench) { + // - for now, only float types are allowed + const parameters = Object.values(workbench).filter(srv => osparc.data.model.Node.isParameter(srv) && srv["key"].includes("parameter/number")); + return parameters; + }, + + extractProbes: function(workbench) { + const parameters = Object.values(workbench).filter(srv => osparc.data.model.Node.isProbe(srv)); + return parameters; + }, + + extractFunctionableProbes: function(workbench) { + // - for now, only float types are allowed + const parameters = Object.values(workbench).filter(srv => osparc.data.model.Node.isProbe(srv) && srv["key"].includes("probe/number")); + return parameters; + }, + + canCreateFunction: function(workbench) { + if (!osparc.store.StaticInfo.getInstance().isDevFeaturesEnabled()) { + return false; + } + + // in order to create a function, the pipeline needs: + // - at least one parameter (or file-picker (file type parameter)) + // - at least one probe + + // const filePickers = osparc.study.Utils.extractFilePickers(workbench); + // const parameters = osparc.study.Utils.extractParameters(workbench); + // const probes = osparc.study.Utils.extractProbes(workbench); + // return (filePickers.length + parameters.length) && probes.length; + + // - for now, only float types are allowed + const parameters = osparc.study.Utils.extractFunctionableParameters(workbench); + const probes = osparc.study.Utils.extractFunctionableProbes(workbench); + return parameters.length && probes.length; + }, + getCantExecuteServices: function(studyServices = []) { return studyServices.filter(studyService => studyService["myAccessRights"]["execute"] === false); },