diff --git a/ci/github/system-testing/e2e.bash b/ci/github/system-testing/e2e.bash index 7f7ef032446..2fc91bb7332 100755 --- a/ci/github/system-testing/e2e.bash +++ b/ci/github/system-testing/e2e.bash @@ -14,10 +14,12 @@ install() { echo "--------------- getting simcore docker images..." make pull-version || ( (make pull-cache || true) && make build tag-version) make info-images + # configure simcore for testing with a private registry - bash tests/e2e/setup_env_insecure_registry - # start simcore - make up-version + bash tests/e2e/scripts/setup_env_insecure_registry.bash + + # start simcore and set log-level to debug + export LOG_LEVEL=INFO; make up-version echo "-------------- installing test framework..." # create a python venv and activate @@ -34,6 +36,7 @@ install() { echo "--------------- transfering the images to the local registry..." make transfer-images-to-registry echo "--------------- injecting templates in postgres db..." + make pg-db-tables make inject-templates-in-db popd } @@ -53,6 +56,9 @@ recover_artifacts() { (docker service logs --timestamps simcore_storage > simcore_logs/storage.log) || true (docker service logs --timestamps simcore_sidecar > simcore_logs/sidecar.log) || true (docker service logs --timestamps simcore_catalog > simcore_logs/catalog.log) || true + + # stack config + (cp .stack-simcore-version.yml simcore_logs/) || true } clean_up() { diff --git a/ci/travis/system-testing/e2e.bash b/ci/travis/system-testing/e2e.bash index 1cddf0ff10d..9cf070ad4ea 100644 --- a/ci/travis/system-testing/e2e.bash +++ b/ci/travis/system-testing/e2e.bash @@ -24,13 +24,15 @@ before_script() { make pull-version || ( (make pull-cache || true) && make build tag-version) make info-images # configure simcore for testing with a private registry - bash tests/e2e/setup_env_insecure_registry - # start simcore - make up-version + bash tests/e2e/scripts/setup_env_insecure_registry.bash + + # start simcore and set log-level to debug + export LOG_LEVEL=INFO; make up-version echo "-------------- installing test framework..." # create a python venv and activate make .venv + # shellcheck disable=SC1091 source .venv/bin/activate bash ci/helpers/ensure_python_pip.bash pushd tests/e2e; @@ -42,6 +44,7 @@ before_script() { echo "--------------- transfering the images to the local registry..." make transfer-images-to-registry echo "--------------- injecting templates in postgres db..." + make pg-db-tables make inject-templates-in-db popd } diff --git a/packages/postgres-database/requirements/prod.txt b/packages/postgres-database/requirements/prod.txt new file mode 100644 index 00000000000..eecfe292a7c --- /dev/null +++ b/packages/postgres-database/requirements/prod.txt @@ -0,0 +1,11 @@ +# Shortcut to install 'simcore-postgres-database' +# +# Usage: +# pip install -r requirements/prod.txt +# + +# installs requirements first +-r _base.txt +-r _migration.txt + +.[migration] diff --git a/services/catalog/src/simcore_service_catalog/db.py b/services/catalog/src/simcore_service_catalog/db.py index e318de15279..675ec192395 100644 --- a/services/catalog/src/simcore_service_catalog/db.py +++ b/services/catalog/src/simcore_service_catalog/db.py @@ -35,6 +35,7 @@ async def teardown_engine() -> None: async def create_tables(conn: SAConnection): + # FIXME: this is dangerous since it enforces an empty table await conn.execute(f"DROP TABLE IF EXISTS {DAG.__tablename__}") await conn.execute(CreateTable(dags)) diff --git a/services/director/tests/fixtures/fake_services.py b/services/director/tests/fixtures/fake_services.py index 1799c4e3462..4ee03852db7 100644 --- a/services/director/tests/fixtures/fake_services.py +++ b/services/director/tests/fixtures/fake_services.py @@ -13,6 +13,7 @@ _logger = logging.getLogger(__name__) + @pytest.fixture(scope="function") def push_services(loop, docker_registry, tmpdir): registry_url = docker_registry @@ -20,19 +21,50 @@ def push_services(loop, docker_registry, tmpdir): list_of_pushed_images_tags = [] dependent_images = [] - def build_push_images(number_of_computational_services, number_of_interactive_services, inter_dependent_services=False, bad_json_format=False, version="1.0."): + + def build_push_images( + number_of_computational_services, + number_of_interactive_services, + inter_dependent_services=False, + bad_json_format=False, + version="1.0.", + ): try: dependent_image = None if inter_dependent_services: - dependent_image = _build_push_image(tmp_dir, registry_url, "computational", "dependency", "10.52.999999", None, bad_json_format=bad_json_format) + dependent_image = _build_push_image( + tmp_dir, + registry_url, + "computational", + "dependency", + "10.52.999999", + None, + bad_json_format=bad_json_format, + ) dependent_images.append(dependent_image) for image_index in range(0, number_of_computational_services): - image = _build_push_image(tmp_dir, registry_url, "computational", "test", version + str(image_index), dependent_image, bad_json_format=bad_json_format) + image = _build_push_image( + tmp_dir, + registry_url, + "computational", + "test", + version + str(image_index), + dependent_image, + bad_json_format=bad_json_format, + ) list_of_pushed_images_tags.append(image) for image_index in range(0, number_of_interactive_services): - image = _build_push_image(tmp_dir, registry_url, "dynamic", "test", version + str(image_index), dependent_image, bad_json_format=bad_json_format) + image = _build_push_image( + tmp_dir, + registry_url, + "dynamic", + "test", + version + str(image_index), + dependent_image, + bad_json_format=bad_json_format, + ) list_of_pushed_images_tags.append(image) except docker.errors.APIError: _logger.exception("Unexpected docker API error") @@ -45,70 +77,110 @@ def build_push_images(number_of_computational_services, number_of_interactive_se _clean_registry(registry_url, list_of_pushed_images_tags) _clean_registry(registry_url, dependent_images) -def _build_push_image(docker_dir, registry_url, service_type, name, tag, dependent_image=None, *, bad_json_format=False): # pylint: disable=R0913 + +def _build_push_image( + docker_dir, + registry_url, + service_type, + name, + tag, + dependent_image=None, + *, + bad_json_format=False +): # pylint: disable=R0913 docker_client = docker.from_env() # crate image service_description = _create_service_description(service_type, name, tag) docker_labels = _create_docker_labels(service_description, bad_json_format) - additional_docker_labels = [{"name": "constraints", "type": "string", "value": ["node.role==manager"]}] + additional_docker_labels = [ + {"name": "constraints", "type": "string", "value": ["node.role==manager"]} + ] internal_port = None - entry_point = '' + entry_point = "" if service_type == "dynamic": internal_port = random.randint(1, 65535) - additional_docker_labels.append({"name": "ports", "type": "int", "value": internal_port}) + additional_docker_labels.append( + {"name": "ports", "type": "int", "value": internal_port} + ) entry_point = "/test/entry_point" - docker_labels["simcore.service.bootsettings"] = json.dumps([{"name": "entry_point", "type": "string", "value": entry_point}]) + docker_labels["simcore.service.bootsettings"] = json.dumps( + [{"name": "entry_point", "type": "string", "value": entry_point}] + ) docker_labels["simcore.service.settings"] = json.dumps(additional_docker_labels) if bad_json_format: - docker_labels["simcore.service.settings"] = "'fjks" + docker_labels["simcore.service.settings"] + docker_labels["simcore.service.settings"] = ( + "'fjks" + docker_labels["simcore.service.settings"] + ) if dependent_image is not None: dependent_description = dependent_image["service_description"] - dependency_docker_labels = [{"key":dependent_description["key"], "tag":dependent_description["version"]}] - docker_labels["simcore.service.dependencies"] = json.dumps(dependency_docker_labels) + dependency_docker_labels = [ + { + "key": dependent_description["key"], + "tag": dependent_description["version"], + } + ] + docker_labels["simcore.service.dependencies"] = json.dumps( + dependency_docker_labels + ) if bad_json_format: - docker_labels["simcore.service.dependencies"] = "'fjks" + docker_labels["simcore.service.dependencies"] + docker_labels["simcore.service.dependencies"] = ( + "'fjks" + docker_labels["simcore.service.dependencies"] + ) image = _create_base_image(docker_dir, docker_labels) # tag image - image_tag = registry_url + "/{key}:{version}".format(key=service_description["key"], version=tag) + image_tag = registry_url + "/{key}:{version}".format( + key=service_description["key"], version=tag + ) assert image.tag(image_tag) is True # push image to registry docker_client.images.push(image_tag) # remove image from host docker_client.images.remove(image_tag) return { - "service_description":service_description, - "docker_labels":docker_labels, - "image_path":image_tag, - "internal_port":internal_port, - "entry_point": entry_point - } + "service_description": service_description, + "docker_labels": docker_labels, + "image_path": image_tag, + "internal_port": internal_port, + "entry_point": entry_point, + } + def _clean_registry(registry_url, list_of_images): - request_headers = {'accept': "application/vnd.docker.distribution.manifest.v2+json"} + request_headers = {"accept": "application/vnd.docker.distribution.manifest.v2+json"} for image in list_of_images: service_description = image["service_description"] # get the image digest tag = service_description["version"] - url = "http://{host}/v2/{name}/manifests/{tag}".format(host=registry_url, name=service_description["key"], tag=tag) + url = "http://{host}/v2/{name}/manifests/{tag}".format( + host=registry_url, name=service_description["key"], tag=tag + ) response = requests.get(url, headers=request_headers) docker_content_digest = response.headers["Docker-Content-Digest"] # remove the image from the registry - url = "http://{host}/v2/{name}/manifests/{digest}".format(host=registry_url, name=service_description["key"], digest=docker_content_digest) + url = "http://{host}/v2/{name}/manifests/{digest}".format( + host=registry_url, + name=service_description["key"], + digest=docker_content_digest, + ) response = requests.delete(url, headers=request_headers) + def _create_base_image(base_dir, labels): # create a basic dockerfile docker_file = base_dir / "Dockerfile" with docker_file.open("w") as file_pointer: - file_pointer.write('FROM alpine\nCMD while true; do sleep 10; done\n') + file_pointer.write("FROM alpine\nCMD while true; do sleep 10; done\n") assert docker_file.exists() == True # build docker base image docker_client = docker.from_env() - base_docker_image = docker_client.images.build(path=str(base_dir), rm=True, labels=labels) + base_docker_image = docker_client.images.build( + path=str(base_dir), rm=True, labels=labels + ) return base_docker_image[0] + def _create_service_description(service_type, name, tag): file_name = "dummy_service_description-v1.json" dummy_description_path = Path(__file__).parent / file_name @@ -125,10 +197,13 @@ def _create_service_description(service_type, name, tag): return service_desc + def _create_docker_labels(service_description, bad_json_format): docker_labels = {} for key, value in service_description.items(): - docker_labels[".".join(["io", "simcore", key])] = json.dumps({key:value}) + docker_labels[".".join(["io", "simcore", key])] = json.dumps({key: value}) if bad_json_format: - docker_labels[".".join(["io", "simcore", key])] = "d32;'" + docker_labels[".".join(["io", "simcore", key])] + docker_labels[".".join(["io", "simcore", key])] = ( + "d32;'" + docker_labels[".".join(["io", "simcore", key])] + ) return docker_labels diff --git a/services/director/tests/test_registry_cache_task.py b/services/director/tests/test_registry_cache_task.py index a096972eb67..ced8f50e479 100644 --- a/services/director/tests/test_registry_cache_task.py +++ b/services/director/tests/test_registry_cache_task.py @@ -4,22 +4,28 @@ import pytest -from simcore_service_director import (config, main, registry_cache_task, - registry_proxy) +from simcore_service_director import config, main, registry_cache_task, registry_proxy @pytest.fixture -def client(loop, aiohttp_client, aiohttp_unused_port, configure_schemas_location, configure_registry_access): +def client( + loop, + aiohttp_client, + aiohttp_unused_port, + configure_schemas_location, + configure_registry_access, +): config.DIRECTOR_REGISTRY_CACHING = True config.DIRECTOR_REGISTRY_CACHING_TTL = 5 # config.DIRECTOR_REGISTRY_CACHING_TTL = 5 app = main.setup_app() - server_kwargs={'port': aiohttp_unused_port(), 'host': 'localhost'} + server_kwargs = {"port": aiohttp_unused_port(), "host": "localhost"} registry_cache_task.setup(app) yield loop.run_until_complete(aiohttp_client(app, server_kwargs=server_kwargs)) + async def test_registry_caching_task(loop, client, push_services): app = client.app assert app @@ -30,17 +36,29 @@ async def test_registry_caching_task(loop, client, push_services): assert registry_cache_task.APP_REGISTRY_CACHE_DATA_KEY in app # check we do not get any repository - list_of_services = await registry_proxy.list_services(app, registry_proxy.ServiceType.ALL) + list_of_services = await registry_proxy.list_services( + app, registry_proxy.ServiceType.ALL + ) assert not list_of_services assert app[registry_cache_task.APP_REGISTRY_CACHE_DATA_KEY] != {} # create services in the registry - pushed_services = push_services(1,1) + pushed_services = push_services( + number_of_computational_services=1, number_of_interactive_services=1 + ) # the services shall be updated await sleep(config.DIRECTOR_REGISTRY_CACHING_TTL) - list_of_services = await registry_proxy.list_services(app, registry_proxy.ServiceType.ALL) + list_of_services = await registry_proxy.list_services( + app, registry_proxy.ServiceType.ALL + ) assert len(list_of_services) == 2 # add more - pushed_services = push_services(2,2, version="2.0.") - await sleep(config.DIRECTOR_REGISTRY_CACHING_TTL) - list_of_services = await registry_proxy.list_services(app, registry_proxy.ServiceType.ALL) - assert len(list_of_services) == len(pushed_services) \ No newline at end of file + pushed_services = push_services( + number_of_computational_services=2, + number_of_interactive_services=2, + version="2.0.", + ) + await sleep(config.DIRECTOR_REGISTRY_CACHING_TTL * 1.1) # NOTE: this sometimes takes a bit more. Sleep increased a 10%. + list_of_services = await registry_proxy.list_services( + app, registry_proxy.ServiceType.ALL + ) + assert len(list_of_services) == len(pushed_services) diff --git a/services/docker-compose.yml b/services/docker-compose.yml index 1167d25479e..374e93c1478 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -10,7 +10,7 @@ services: - POSTGRES_HOST=${POSTGRES_HOST} - POSTGRES_PORT=${POSTGRES_PORT} - TESTING=false - - LOGLEVEL=WARNING + - LOGLEVEL=${LOG_LEVEL:-WARNING} depends_on: - postgres networks: @@ -64,7 +64,7 @@ services: - STORAGE_PORT=8080 - SWARM_STACK_NAME=${SWARM_STACK_NAME:-simcore} - WEBSERVER_MONITORING_ENABLED=1 - - WEBSERVER_LOGLEVEL=WARNING + - WEBSERVER_LOGLEVEL=${LOG_LEVEL:-WARNING} env_file: - ../.env depends_on: @@ -118,7 +118,7 @@ services: - REGISTRY_USER=${REGISTRY_USER} - REGISTRY_PW=${REGISTRY_PW} - SWARM_STACK_NAME=${SWARM_STACK_NAME:-simcore} - - SIDECAR_LOGLEVEL=WARNING + - SIDECAR_LOGLEVEL=${LOG_LEVEL:-WARNING} depends_on: - rabbit - postgres @@ -135,7 +135,7 @@ services: - POSTGRES_DB=${POSTGRES_DB} - POSTGRES_HOST=${POSTGRES_HOST} - POSTGRES_PORT=${POSTGRES_PORT} - - STORAGE_LOGLEVEL=WARNING + - STORAGE_LOGLEVEL=${LOG_LEVEL:-WARNING} - STORAGE_MONITORING_ENABLED=1 - S3_ENDPOINT=${S3_ENDPOINT} - S3_ACCESS_KEY=${S3_ACCESS_KEY} diff --git a/services/storage/tests/test_configs.py b/services/storage/tests/test_configs.py index cc1b1caa200..184fcb60a24 100644 --- a/services/storage/tests/test_configs.py +++ b/services/storage/tests/test_configs.py @@ -45,6 +45,31 @@ def devel_environ(env_devel_file): return env_devel +variable_expansion_pattern = re.compile(r"\$\{*(\w+)+[:-]*(\w+)*\}") + +@pytest.mark.parametrize( + "sample,expected_match", + [ + (r"${varname:-default}", ("varname", "default")), + (r"${varname}", ("varname", None)), + (r"33", None), + (r"${VAR_name:-33}", ("VAR_name", "33")), + (r"${varname-default}", ("varname", "default")), # this is not standard! + (r"${varname:default}", ("varname", "default")), # this is not standard! + ], +) +def test_variable_expansions(sample, expected_match): + # TODO: extend variable expansions + # https://en.wikibooks.org/wiki/Bourne_Shell_Scripting/Variable_Expansion + match = variable_expansion_pattern.match(sample) + if expected_match: + assert match + varname, default = match.groups() + assert (varname, default) == expected_match + else: + assert not match + + @pytest.fixture("session") def container_environ( services_docker_compose_file, devel_environ, osparc_simcore_root_dir @@ -62,15 +87,14 @@ def container_environ( ) environ_items = dc["services"][THIS_SERVICE].get("environment", list()) - MATCH = re.compile(r"\$\{(\w+)+") for item in environ_items: key, value = item.split("=") - m = MATCH.match(value) - if m: - envkey = m.groups()[0] - value = devel_environ[envkey] + match = variable_expansion_pattern.match(value) + if match: + varname, default_value = match.groups() + value = devel_environ.get(varname, default_value) container_environ[key] = value return container_environ diff --git a/services/web/client/source/boot/index.html b/services/web/client/source/boot/index.html index fb80d86895b..ebaabd8e5be 100644 --- a/services/web/client/source/boot/index.html +++ b/services/web/client/source/boot/index.html @@ -41,5 +41,7 @@ + ${preBootJs} + diff --git a/services/web/client/source/class/osparc/component/export/ExportGroup.js b/services/web/client/source/class/osparc/component/export/ExportGroup.js new file mode 100644 index 00000000000..d50ad948f4f --- /dev/null +++ b/services/web/client/source/class/osparc/component/export/ExportGroup.js @@ -0,0 +1,246 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2020 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Odei Maiz (odeimaiz) + */ + +/** + * Widget for exporting nodes-group: + * - Creates a copy of the inner nodes, so that values and access levels can be modified + * - If any of the inner nodes was connected to a non inner node, that connection is removed + * - The exported group is added to the catalog + */ + +qx.Class.define("osparc.component.export.ExportGroup", { + extend: qx.ui.core.Widget, + + /** + * @param node {osparc.data.model.Node} Group Node to be exported + */ + construct: function(node) { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(5)); + + this.set({ + inputNode: node + }); + + this.__prepareOutputNode(); + this.__prepareOutputWorkbench(); + this.__buildLayout(); + }, + + events: { + "finished": "qx.event.type.Data" + }, + + properties: { + inputNode: { + check: "osparc.data.model.Node", + nullable: false + }, + + outputNode: { + check: "osparc.data.model.Node", + nullable: false + }, + + outputWorkbench: { + check: "osparc.data.model.Workbench", + nullable: false + } + }, + + members: { + __groupName: null, + __groupDesc: null, + __activeStudy: null, + + tearDown: function() { + osparc.store.Store.getInstance().setCurrentStudy(this.__activeStudy); + }, + + __prepareOutputNode: function() { + const inputNode = this.getInputNode(); + + const key = inputNode.getKey(); + const version = inputNode.getVersion(); + const nodeData = inputNode.serialize(); + const nodesGroup = new osparc.data.model.Node(key, version); + nodesGroup.populateInputOutputData(nodeData); + this.setOutputNode(nodesGroup); + }, + + __prepareOutputWorkbench: function() { + const inputNode = this.getInputNode(); + + const studydata = { + workbench: this.__groupToWorkbenchData(inputNode) + }; + const dummyStudy = new osparc.data.model.Study(studydata); + + this.__activeStudy = osparc.store.Store.getInstance().getCurrentStudy(); + osparc.store.Store.getInstance().setCurrentStudy(dummyStudy); + this.setOutputWorkbench(dummyStudy.getWorkbench()); + dummyStudy.getWorkbench().buildWorkbench(); + }, + + __buildLayout: function() { + const { + formRenderer, + manager + } = this.__buildMetaDataForm(); + this._add(formRenderer); + + const scroll = new qx.ui.container.Scroll(); + const settingsLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + const settingsView = this.__buildOutputSettings(); + settingsLayout.add(settingsView, { + flex: 1 + }); + scroll.add(settingsLayout); + this._add(scroll, { + flex: 1 + }); + + const exportBtn = new qx.ui.toolbar.Button(this.tr("Export")); + exportBtn.addListener("execute", () => { + if (manager.validate()) { + this.__exportAsMacroService(exportBtn); + } + }, this); + const actionsBar = new qx.ui.toolbar.ToolBar(); + const actionsPart = new qx.ui.toolbar.Part(); + actionsBar.addSpacer(); + actionsPart.add(exportBtn); + actionsBar.add(actionsPart); + this._add(actionsBar); + }, + + __buildMetaDataForm: function() { + const manager = new qx.ui.form.validation.Manager(); + const metaDataForm = new qx.ui.form.Form(); + + const groupName = this.__groupName = new qx.ui.form.TextField(this.getInputNode().getLabel()); + groupName.setRequired(true); + manager.add(groupName); + metaDataForm.add(groupName, this.tr("Name")); + + const groupDesc = this.__groupDesc = new qx.ui.form.TextField(); + metaDataForm.add(groupDesc, this.tr("Description")); + + const formRenderer = new qx.ui.form.renderer.Single(metaDataForm).set({ + padding: 10 + }); + + return { + formRenderer, + manager + }; + }, + + __buildOutputSettings: function() { + const innerNodes = this.getOutputWorkbench().getNodes(true); + const settingsEditorLayout = osparc.component.node.GroupNodeView.getSettingsEditorLayout(innerNodes); + return settingsEditorLayout; + }, + + __exportAsMacroService: function(exportBtn) { + exportBtn.setIcon("@FontAwesome5Solid/circle-notch/12"); + exportBtn.getChildControl("icon").getContentElement() + .addClass("rotate"); + + const outputNode = this.getOutputNode(); + const outputWorkbench = this.getOutputWorkbench(); + + const nodeKey = "simcore/services/frontend/nodes-group/macros/" + outputNode.getNodeId(); + const version = "1.0.0"; + const nodesGroupService = osparc.utils.Services.getNodesGroup(); + nodesGroupService["key"] = nodeKey; + nodesGroupService["version"] = version; + nodesGroupService["name"] = this.__groupName.getValue(); + nodesGroupService["description"] = this.__groupDesc.getValue(); + nodesGroupService["contact"] = osparc.auth.Data.getInstance().getEmail(); + nodesGroupService["workbench"] = outputWorkbench.serializeWorkbench(); + + // Use editorValues + const innerNodes = this.getOutputWorkbench().getNodes(true); + const nodes = Object.values(innerNodes); + for (const node of nodes) { + const nodeEntry = nodesGroupService["workbench"][node.getNodeId()]; + for (let [portId, portValue] of Object.entries(node.getInputEditorValues())) { + nodeEntry.inputs[portId] = portValue; + } + } + osparc.data.Resources.fetch("groups", "post", {data: nodesGroupService}) + .then(data => { + const text = this.tr("Group added to the Service catalog"); + osparc.component.message.FlashMessenger.getInstance().logAs(text, "INFO"); + this.fireDataEvent("finished"); + }) + .catch(err => { + console.error("error creating group", err); + const text = this.tr("Something went wrong adding the Group to the Service catalog"); + osparc.component.message.FlashMessenger.getInstance().logAs(text, "ERROR"); + }) + .finally(() => { + exportBtn.resetIcon(); + exportBtn.getChildControl("icon").getContentElement() + .removeClass("rotate"); + }); + }, + + __groupToWorkbenchData: function(nodesGroup) { + let workbenchData = {}; + + // serialize innerNodes + const innerNodes = nodesGroup.getInnerNodes(true); + Object.values(innerNodes).forEach(innerNode => { + workbenchData[innerNode.getNodeId()] = innerNode.serialize(); + }); + + // remove parent from first level + const firstLevelNodes = nodesGroup.getInnerNodes(false); + Object.values(firstLevelNodes).forEach(firstLevelNode => { + workbenchData[firstLevelNode.getNodeId()]["parent"] = null; + }); + + // deep copy workbenchData + workbenchData = osparc.utils.Utils.deepCloneObject(workbenchData); + + // removeOutReferences + workbenchData = this.__removeOutReferences(workbenchData); + + // replace Uuids + workbenchData = osparc.data.Converters.replaceUuids(workbenchData); + + return workbenchData; + }, + + __removeOutReferences: function(workbench) { + const innerNodeIds = Object.keys(workbench); + for (const nodeId in workbench) { + const node = workbench[nodeId]; + const inputNodes = node.inputNodes; + for (let i=0; i label + ": " + fromPortLabel - }); - - this.fireDataEvent("linkAdded", toPortId); - - return true; - }, - - removeLink: function(toPortId) { - this.getControl(toPortId).setEnabled(true); - if ("link" in this.getControl(toPortId)) { - delete this.getControl(toPortId).link; - } - - this.fireDataEvent("linkRemoved", toPortId); } } }); diff --git a/services/web/client/source/class/osparc/component/form/ToggleButtonContainer.js b/services/web/client/source/class/osparc/component/form/ToggleButtonContainer.js index f63e671ce48..8dd438bc7ae 100644 --- a/services/web/client/source/class/osparc/component/form/ToggleButtonContainer.js +++ b/services/web/client/source/class/osparc/component/form/ToggleButtonContainer.js @@ -20,7 +20,7 @@ qx.Class.define("osparc.component.form.ToggleButtonContainer", { }, members: { - // overriden + // overridden add: function(child, options) { if (child instanceof qx.ui.form.ToggleButton) { this.base(arguments, child, options); diff --git a/services/web/client/source/class/osparc/component/form/renderer/PropForm.js b/services/web/client/source/class/osparc/component/form/renderer/PropForm.js index af46af0faef..949ff75e247 100644 --- a/services/web/client/source/class/osparc/component/form/renderer/PropForm.js +++ b/services/web/client/source/class/osparc/component/form/renderer/PropForm.js @@ -6,7 +6,7 @@ Utf8Check: äöü ************************************************************************ */ -/* eslint no-underscore-dangle: ["error", { "allowAfterThis": true, "allow": ["__ctrlMap"] }] */ +/* eslint no-underscore-dangle: ["error", { "allowAfterThis": true}] */ /** * A special renderer for AutoForms which includes notes below the section header @@ -14,41 +14,26 @@ */ qx.Class.define("osparc.component.form.renderer.PropForm", { - extend : qx.ui.form.renderer.Single, + extend: osparc.component.form.renderer.PropFormBase, + /** - * create a page for the View Tab with the given title - * - * @param vizWidget {Widget} visualization widget to embedd - */ + * create a page for the View Tab with the given title + * + * @param form {osparc.component.form.Auto} form widget to embedd + * @param node {osparc.data.model.Node} Node owning the widget + */ construct: function(form, node) { - if (node) { - this.setNode(node); - } else { - this.setNode(null); - } + this.base(arguments, form, node); - this.base(arguments, form); - let fl = this._getLayout(); - // have plenty of space for input, not for the labels - fl.setColumnFlex(0, 0); - fl.setColumnAlign(0, "left", "top"); - fl.setColumnFlex(1, 1); - fl.setColumnMinWidth(1, 130); + this.__ctrlLinkMap = {}; + this.__addLinkCtrls(); this.setDroppable(true); this.__attachDragoverHighlighter(); }, events: { - "removeLink" : "qx.event.type.Data", - "dataFieldModified": "qx.event.type.Data" - }, - - properties: { - node: { - check: "osparc.data.model.Node", - nullable: true - } + "linkModified": "qx.event.type.Data" }, statics: { @@ -69,9 +54,10 @@ qx.Class.define("osparc.component.form.renderer.PropForm", { // eslint-disable-next-line qx-rules/no-refs-in-members members: { + // overridden _gridPos: { label: 0, - entryField: 1, + ctrlField: 1, retrieveStatus: 2 }, _retrieveStatus: { @@ -80,45 +66,14 @@ qx.Class.define("osparc.component.form.renderer.PropForm", { retrieving: 1, succeed: 2 }, - addItems: function(items, names, title, itemOptions, headerOptions) { - // add the header - if (title !== null) { - this._add( - this._createHeader(title), { - row: this._row, - column: this._gridPos.label, - colSpan: Object.keys(this._gridPos).length - } - ); - this._row++; - } - // add the items - for (let i = 0; i < items.length; i++) { - let item = items[i]; - let label = this._createLabel(names[i], item); - this._add(label, { - row: this._row, - column: this._gridPos.label - }); - label.setBuddy(item); + __ctrlLinkMap: null, - const field = new osparc.component.form.FieldWHint(null, item.description, item); - field.key = item.key; - this._add(field, { - row: this._row, - column: this._gridPos.entryField - }); - this._row++; - this._connectVisibility(item, label); - // store the names for translation - if (qx.core.Environment.get("qx.dynlocale")) { - this._names.push({ - name: names[i], - label: label, - item: items[i] - }); - } + // overridden + addItems: function(items, names, title, itemOptions, headerOptions) { + this.base(arguments, items, names, title, itemOptions, headerOptions); + + items.forEach(item => { this.__createDropMechanism(item, item.key); // Notify focus and focus out @@ -134,68 +89,29 @@ qx.Class.define("osparc.component.form.renderer.PropForm", { qx.event.message.Bus.getInstance().dispatchByName("inputFocusout", msgDataFn); } }, this); - } + }); }, - getValues: function() { - let data = this._form.getData(); - for (const portId in data) { - let ctrl = this._form.getControl(portId); - if (ctrl && ctrl.link) { - data[portId] = ctrl.link; - } - // FIXME: "null" should be a valid input - if (data[portId] === "null") { - data[portId] = null; - } - } - let filteredData = {}; + // overridden + setAccessLevel: function(data) { for (const key in data) { - if (data[key] !== null) { - filteredData[key] = data[key]; - } - } - return filteredData; - }, + const control = this._form.getControl(key); + this.__changeControlVisibility(control, data[key]); - __getLayoutChild(portId, column) { - let row = null; - const children = this._getChildren(); - for (let i=0; i { this.__unhighlightAll(); }); + }, + + getControlLinks: function() { + return this.__ctrlLinkMap; + }, + + getControlLink: function(key) { + return this.__ctrlLinkMap[key]; + }, + + __addLinkCtrls: function() { + Object.keys(this._form.getControls()).forEach(portId => { + this.__addLinkCtrl(portId); + }); + }, + + __addLinkCtrl: function(portId) { + const controlLink = new qx.ui.form.TextField().set({ + enabled: false + }); + controlLink.key = portId; + this.__ctrlLinkMap[portId] = controlLink; + }, + + __isPortAvailable: function(portId) { + const port = this._form.getControl(portId); + if (!port || !port.getEnabled() || Object.prototype.hasOwnProperty.call(port, "link")) { + return false; + } + return true; + }, + + addLink: function(toPortId, fromNodeId, fromPortId) { + if (!this.__isPortAvailable(toPortId)) { + return false; + } + this.getControlLink(toPortId).setEnabled(false); + this._form.getControl(toPortId).link = { + nodeUuid: fromNodeId, + output: fromPortId + }; + + const study = osparc.store.Store.getInstance().getCurrentStudy(); + const workbench = study.getWorkbench(); + const fromNode = workbench.getNode(fromNodeId); + const port = fromNode.getOutput(fromPortId); + const fromPortLabel = port ? port.label : null; + fromNode.bind("label", this.getControlLink(toPortId), "value", { + converter: label => label + ": " + fromPortLabel + }); + + this.linkAdded(toPortId); + + return true; + }, + + addLinks: function(data) { + for (let key in data) { + if (data[key] !== null && typeof data[key] === "object" && data[key].nodeUuid) { + this.addLink(key, data[key].nodeUuid, data[key].output); + } + } + }, + + removeLink: function(toPortId) { + this.getControlLink(toPortId).setEnabled(false); + if ("link" in this._form.getControl(toPortId)) { + delete this._form.getControl(toPortId).link; + } + + this.linkRemoved(toPortId); + }, + + hasVisibleInputs: function() { + const children = this._getChildren(); + for (let i=0; i { + this.__addAccessLevelRB(portId); + }); + }, + + __addAccessLevelRB: function(portId) { + const rbHidden = new qx.ui.form.RadioButton(this.tr("Not Visible")); + rbHidden.accessLevel = this._visibility.hidden; + rbHidden.portId = portId; + const rbReadOnly = new qx.ui.form.RadioButton(this.tr("Read Only")); + rbReadOnly.accessLevel = this._visibility.readOnly; + rbReadOnly.portId = portId; + const rbEditable = new qx.ui.form.RadioButton(this.tr("Editable")); + rbEditable.accessLevel = this._visibility.readWrite; + rbEditable.portId = portId; + + const groupBox = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + groupBox.add(rbHidden); + groupBox.add(rbReadOnly); + groupBox.add(rbEditable); + + const group = new qx.ui.form.RadioGroup(rbHidden, rbReadOnly, rbEditable); + group.setSelection([rbEditable]); + this.__ctrlRBsMap[portId] = group; + group.addListener("changeSelection", this.__onAccessLevelChanged, this); + + const ctrlField = this._getCtrlFieldChild(portId); + if (ctrlField) { + const idx = ctrlField.idx; + const child = ctrlField.child; + const layoutProps = child.getLayoutProperties(); + this._addAt(groupBox, idx, { + row: layoutProps.row, + column: this._gridPos.accessLevel + }); + } + }, + + __onAccessLevelChanged: function(e) { + const selectedButton = e.getData()[0]; + const { + accessLevel, + portId + } = selectedButton; + + const data = {}; + data[portId] = accessLevel; + + this.__setAccessLevel(data); + + let inputAccess = this.getNode().getInputAccess(); + if (inputAccess === null) { + inputAccess = {}; + } + inputAccess[portId] = accessLevel; + this.getNode().setInputAccess(inputAccess); + + const propWidget = this.getNode().getPropsWidget(); + propWidget.setAccessLevel(data); + }, + + __addDelTag: function(label) { + const newLabel = "" + label + ""; + return newLabel; + }, + + __removeDelTag: function(label) { + let newLabel = label.replace("", ""); + newLabel = newLabel.replace("", ""); + return newLabel; + }, + + __setAccessLevel: function(data) { + for (const key in data) { + const label = this._getLabelFieldChild(key).child; + const control = this._form.getControl(key); + switch (data[key]) { + case this._visibility.hidden: { + const newLabel = this.__addDelTag(label.getValue()); + label.setValue(newLabel); + label.setEnabled(false); + control.setEnabled(false); + break; + } + case this._visibility.readOnly: { + const newLabel = this.__removeDelTag(label.getValue()); + label.setValue(newLabel); + label.setEnabled(false); + control.setEnabled(false); + break; + } + case this._visibility.readWrite: { + const newLabel = this.__removeDelTag(label.getValue()); + label.setValue(newLabel); + label.setEnabled(true); + control.setEnabled(true); + break; + } + } + } + }, + + __getRadioButtonsFieldChild: function(portId) { + return this._getLayoutChild(portId, this._gridPos.accessLevel); + } + } +}); diff --git a/services/web/client/source/class/osparc/component/metadata/ServiceInfo.js b/services/web/client/source/class/osparc/component/metadata/ServiceInfo.js index 750164c310a..fb7828cf386 100644 --- a/services/web/client/source/class/osparc/component/metadata/ServiceInfo.js +++ b/services/web/client/source/class/osparc/component/metadata/ServiceInfo.js @@ -145,7 +145,7 @@ qx.Class.define("osparc.component.metadata.ServiceInfo", { __createDescription: function() { const description = new osparc.ui.markdown.Markdown(); - description.setMarkdown(this.__metadata.description); + description.setMarkdown(this.__metadata.description || ""); return description; }, diff --git a/services/web/client/source/class/osparc/component/node/BaseNodeView.js b/services/web/client/source/class/osparc/component/node/BaseNodeView.js index 9c37d3c165a..48f5a6e9d3e 100644 --- a/services/web/client/source/class/osparc/component/node/BaseNodeView.js +++ b/services/web/client/source/class/osparc/component/node/BaseNodeView.js @@ -35,11 +35,24 @@ qx.Class.define("osparc.component.node.BaseNodeView", { createSettingsGroupBox: function(label) { const settingsGroupBox = new qx.ui.groupbox.GroupBox(label).set({ appearance: "settings-groupbox", - maxWidth: 500, + maxWidth: 800, alignX: "center", layout: new qx.ui.layout.VBox() }); return settingsGroupBox; + }, + + createWindow: function(label) { + const win = new qx.ui.window.Window(label).set({ + layout: new qx.ui.layout.Grow(), + contentPadding: 10, + showMinimize: false, + resizable: true, + modal: true, + height: 600, + width: 800 + }); + return win; } }, @@ -65,6 +78,14 @@ qx.Class.define("osparc.component.node.BaseNodeView", { _buttonContainer: null, _filesButton: null, + populateLayout: function() { + this.getNode().bind("label", this._title, "value"); + this._addInputPortsUIs(); + this._addSettings(); + this._addIFrame(); + this._addButtons(); + }, + __buildInputsView: function() { const inputsView = this._inputsView = new osparc.desktop.SidePanel().set({ minWidth: 300 @@ -123,19 +144,6 @@ qx.Class.define("osparc.component.node.BaseNodeView", { inputFont: "text-18", editable: osparc.data.Permissions.getInstance().canDo("study.node.rename") }); - titlePart.add(title); - - const infoBtn = new qx.ui.toolbar.Button(this.tr("Info"), "@FontAwesome5Solid/info-circle/14"); - infoPart.add(infoBtn); - - const filesBtn = this._filesButton = new qx.ui.toolbar.Button(this.tr("Files"), "@FontAwesome5Solid/folder-open/14"); - osparc.utils.Utils.setIdToWidget(filesBtn, "nodeViewFilesBtn"); - buttonsPart.add(filesBtn); - - filesBtn.addListener("execute", () => this.__openNodeDataManager(), this); - - infoBtn.addListener("execute", () => this.__openServiceInfo(), this); - title.addListener("editValue", evt => { if (evt.getData() !== this._title.getValue()) { const node = this.getNode(); @@ -146,6 +154,22 @@ qx.Class.define("osparc.component.node.BaseNodeView", { qx.event.message.Bus.getInstance().dispatchByName("updateStudy", study.serializeStudy()); } }, this); + titlePart.add(title); + + const infoBtn = new qx.ui.toolbar.Button(this.tr("Info"), "@FontAwesome5Solid/info-circle/14"); + infoBtn.addListener("execute", () => this.__openServiceInfo(), this); + infoPart.add(infoBtn); + + if (osparc.data.Permissions.getInstance().canDo("study.node.update")) { + const editAccessLevel = new qx.ui.toolbar.Button(this.tr("Edit Access Level")); + editAccessLevel.addListener("execute", () => this._openEditAccessLevel(), this); + infoPart.add(editAccessLevel); + } + + const filesBtn = this._filesButton = new qx.ui.toolbar.Button(this.tr("Files"), "@FontAwesome5Solid/folder-open/14"); + osparc.utils.Utils.setIdToWidget(filesBtn, "nodeViewFilesBtn"); + filesBtn.addListener("execute", () => this.__openNodeDataManager(), this); + buttonsPart.add(filesBtn); return toolbar; }, @@ -204,10 +228,6 @@ qx.Class.define("osparc.component.node.BaseNodeView", { if (retrieveIFrameButton) { this._buttonContainer.add(retrieveIFrameButton); } - let restartIFrameButton = this.getNode().getRestartIFrameButton(); - if (restartIFrameButton) { - this._buttonContainer.add(restartIFrameButton); - } this._buttonContainer.add(this._filesButton); this._toolbar.add(this._buttonContainer); }, @@ -270,6 +290,27 @@ qx.Class.define("osparc.component.node.BaseNodeView", { }, this); }, + /** + * @abstract + */ + _addSettings: function() { + throw new Error("Abstract method called!"); + }, + + /** + * @abstract + */ + _addIFrame: function() { + throw new Error("Abstract method called!"); + }, + + /** + * @abstract + */ + _openEditAccessLevel: function() { + throw new Error("Abstract method called!"); + }, + /** * @abstract * @param node {osparc.data.model.Node} node diff --git a/services/web/client/source/class/osparc/component/node/GroupNodeView.js b/services/web/client/source/class/osparc/component/node/GroupNodeView.js index 9a7d4683036..9c6e2aef4fd 100644 --- a/services/web/client/source/class/osparc/component/node/GroupNodeView.js +++ b/services/web/client/source/class/osparc/component/node/GroupNodeView.js @@ -36,26 +36,35 @@ qx.Class.define("osparc.component.node.GroupNodeView", { this.base(arguments); }, - members: { - populateLayout: function() { - this.getNode().bind("label", this._title, "value"); - this._addInputPortsUIs(); - this.__addSettings(); - this.__addIFrame(); - this._addButtons(); - }, + statics: { + getSettingsEditorLayout: function(nodes) { + const settingsEditorLayout = osparc.component.node.BaseNodeView.createSettingsGroupBox("Settings"); + Object.values(nodes).forEach(innerNode => { + const propsWidgetEditor = innerNode.getPropsWidgetEditor(); + if (propsWidgetEditor && Object.keys(innerNode.getInputs()).length) { + const innerSettings = osparc.component.node.BaseNodeView.createSettingsGroupBox().set({ + maxWidth: 700 + }); + innerNode.bind("label", innerSettings, "legend"); + innerSettings.add(propsWidgetEditor); + settingsEditorLayout.add(innerSettings); + } + }); + return settingsEditorLayout; + } + }, - __addSettings: function() { + members: { + _addSettings: function() { this._settingsLayout.removeAll(); this._mapperLayout.removeAll(); const innerNodes = this.getNode().getInnerNodes(true); Object.values(innerNodes).forEach(innerNode => { - // const innerSettings = this.superclass.self().createSettingsGroupBox(); - const innerSettings = osparc.component.node.BaseNodeView.createSettingsGroupBox(); - innerNode.bind("label", innerSettings, "legend"); const propsWidget = innerNode.getPropsWidget(); - if (propsWidget && Object.keys(innerNode.getInputs()).length) { + if (propsWidget && Object.keys(innerNode.getInputs()).length && propsWidget.hasVisibleInputs()) { + const innerSettings = osparc.component.node.BaseNodeView.createSettingsGroupBox(); + innerNode.bind("label", innerSettings, "legend"); innerSettings.add(propsWidget); this._settingsLayout.add(innerSettings); } @@ -73,7 +82,7 @@ qx.Class.define("osparc.component.node.GroupNodeView", { }); }, - __addIFrame: function() { + _addIFrame: function() { this._iFrameLayout.removeAll(); const tabView = new qx.ui.tabview.TabView().set({ @@ -109,6 +118,14 @@ qx.Class.define("osparc.component.node.GroupNodeView", { }); }, + _openEditAccessLevel: function() { + const settingsEditorLayout = this.self().getSettingsEditorLayout(this.getNode().getInnerNodes()); + const win = osparc.component.node.BaseNodeView.createWindow(this.getNode().getLabel()); + win.add(settingsEditorLayout); + win.center(); + win.open(); + }, + _applyNode: function(node) { if (!node.isContainer()) { console.error("Only group nodes are supported"); diff --git a/services/web/client/source/class/osparc/component/node/NodeView.js b/services/web/client/source/class/osparc/component/node/NodeView.js index de19d9dbd09..bf4d50af30b 100644 --- a/services/web/client/source/class/osparc/component/node/NodeView.js +++ b/services/web/client/source/class/osparc/component/node/NodeView.js @@ -42,15 +42,7 @@ qx.Class.define("osparc.component.node.NodeView", { }, members: { - populateLayout: function() { - this.getNode().bind("label", this._title, "value"); - this._addInputPortsUIs(); - this.__addSettings(); - this.__addIFrame(); - this._addButtons(); - }, - - __addSettings: function() { + _addSettings: function() { this._settingsLayout.removeAll(); this._mapperLayout.removeAll(); @@ -72,7 +64,7 @@ qx.Class.define("osparc.component.node.NodeView", { }); }, - __addIFrame: function() { + _addIFrame: function() { this._iFrameLayout.removeAll(); const iFrame = this.getNode().getIFrame(); @@ -94,6 +86,16 @@ qx.Class.define("osparc.component.node.NodeView", { }); }, + _openEditAccessLevel: function() { + const settingsEditorLayout = osparc.component.node.BaseNodeView.createSettingsGroupBox(this.tr("Settings")); + settingsEditorLayout.add(this.getNode().getPropsWidgetEditor()); + + const win = osparc.component.node.BaseNodeView.createWindow(this.getNode().getLabel()); + win.add(settingsEditorLayout); + win.center(); + win.open(); + }, + _applyNode: function(node) { if (node.isContainer()) { console.error("Only non-group nodes are supported"); diff --git a/services/web/client/source/class/osparc/component/widget/InputsMapper.js b/services/web/client/source/class/osparc/component/widget/InputsMapper.js index 7b5082fa03e..32a6f61018a 100644 --- a/services/web/client/source/class/osparc/component/widget/InputsMapper.js +++ b/services/web/client/source/class/osparc/component/widget/InputsMapper.js @@ -184,8 +184,8 @@ qx.Class.define("osparc.component.widget.InputsMapper", { let newItemBranch = qx.data.marshal.Json.createModel(newBranch, true); const itemProps = osparc.dev.fake.Data.getItem(null, Object.keys(node.getInputsDefault())[0], defValueId); if (itemProps) { - let form = new osparc.component.form.Auto(itemProps, this.getNode()); - let propsWidget = new osparc.component.form.renderer.PropForm(form); + let form = new osparc.component.form.Auto(itemProps); + let propsWidget = new osparc.component.form.renderer.PropForm(form, this.getNode()); newItemBranch["propsWidget"] = propsWidget; } data.children.push(newItemBranch); @@ -274,8 +274,8 @@ qx.Class.define("osparc.component.widget.InputsMapper", { // Hmmmm not sure about the double getKey :( const itemProps = osparc.dev.fake.Data.getItem(null, fromPortKey, newItem.getKey().getKey()); if (itemProps) { - let form = new osparc.component.form.Auto(itemProps, this.getNode()); - let propsWidget = new osparc.component.form.renderer.PropForm(form); + let form = new osparc.component.form.Auto(itemProps); + let propsWidget = new osparc.component.form.renderer.PropForm(form, this.getNode()); newItem["propsWidget"] = propsWidget; } } diff --git a/services/web/client/source/class/osparc/component/widget/NodeInOut.js b/services/web/client/source/class/osparc/component/widget/NodeInOut.js index 13fc623bb00..8ac580f44ea 100644 --- a/services/web/client/source/class/osparc/component/widget/NodeInOut.js +++ b/services/web/client/source/class/osparc/component/widget/NodeInOut.js @@ -102,7 +102,7 @@ qx.Class.define("osparc.component.widget.NodeInOut", { this.emptyPorts(); const metaData = this.getNode().getMetaData(); - this.__createUIPorts(isInput, metaData.outputs); + this.__createUIPorts(isInput, metaData && metaData.outputs); }, __createUIPorts: function(isInput, ports) { diff --git a/services/web/client/source/class/osparc/component/widget/NodesTree.js b/services/web/client/source/class/osparc/component/widget/NodesTree.js index ae5de475187..fe84dda2f2c 100644 --- a/services/web/client/source/class/osparc/component/widget/NodesTree.js +++ b/services/web/client/source/class/osparc/component/widget/NodesTree.js @@ -171,8 +171,12 @@ qx.Class.define("osparc.component.widget.NodesTree", { createItem: () => new osparc.component.widget.NodeTreeItem(), bindItem: (c, item, id) => { c.bindDefaultProperties(item, id); - c.bindProperty("label", "label", null, item, id); c.bindProperty("nodeId", "nodeId", null, item, id); + const node = study.getWorkbench().getNode(item.getModel().getNodeId()); + if (node) { + node.bind("label", item.getModel(), "label"); + } + c.bindProperty("label", "label", null, item, id); }, configureItem: item => { item.addListener("dbltap", () => { @@ -236,7 +240,6 @@ qx.Class.define("osparc.component.widget.NodesTree", { if (selectedItem) { if (selectedItem.getIsContainer()) { const nodeId = selectedItem.getNodeId(); - this.__openItem(nodeId); this.fireDataEvent("exportNode", nodeId); } else { osparc.component.message.FlashMessenger.getInstance().logAs(this.tr("Only Groups can be exported."), "ERROR"); diff --git a/services/web/client/source/class/osparc/component/widget/PersistentIframe.js b/services/web/client/source/class/osparc/component/widget/PersistentIframe.js index 1a68e5b7d06..58fb791e3f5 100644 --- a/services/web/client/source/class/osparc/component/widget/PersistentIframe.js +++ b/services/web/client/source/class/osparc/component/widget/PersistentIframe.js @@ -27,8 +27,8 @@ qx.Class.define("osparc.component.widget.PersistentIframe", { construct: function(source, el) { this.base(arguments, source); }, - properties : - { + + properties: { /** * Show a Maximize Button */ @@ -38,15 +38,20 @@ qx.Class.define("osparc.component.widget.PersistentIframe", { apply: "_applyShowMaximize" } }, + events: { + /** Fired for requesting a restart */ + "restart" : "qx.event.type.Event", /** Fired if the iframe is restored from a minimized or maximized state */ "restore" : "qx.event.type.Event", /** Fired if the iframe is maximized */ "maximize" : "qx.event.type.Event" }, + members: { __iframe: null, __syncScheduled: null, + __restartButton: null, __actionButton: null, // override _createContentElement : function() { @@ -63,6 +68,22 @@ qx.Class.define("osparc.component.widget.PersistentIframe", { appRoot.add(iframe, { top:-10000 }); + const restartButton = this.__restartButton = new qx.ui.form.Button(null, "@FontAwesome5Solid/redo-alt/14").set({ + zIndex: 20, + paddingLeft: 8, + paddingRight: 8, + paddingTop: 6, + paddingBottom: 6, + backgroundColor: "transparent", + decorator: null + }); + restartButton.addListener("execute", e => { + this.fireEvent("restart"); + }, this); + osparc.utils.Utils.setIdToWidget(restartButton, "iFrameRestartBtn"); + appRoot.add(restartButton, { + top:-10000 + }); let actionButton = this.__actionButton = new qx.ui.form.Button(null, osparc.theme.osparcdark.Image.URLS["window-maximize"]+"/20").set({ zIndex: 20, backgroundColor: "transparent", @@ -83,6 +104,9 @@ qx.Class.define("osparc.component.widget.PersistentIframe", { iframe.setLayoutProperties({ top: -10000 }); + restartButton.setLayoutProperties({ + top: -10000 + }); actionButton.setLayoutProperties({ top: -10000 }); @@ -129,8 +153,12 @@ qx.Class.define("osparc.component.widget.PersistentIframe", { window.setTimeout(() => { this.__syncScheduled = false; let iframeParentPos = qx.bom.element.Location.get(qx.bom.element.Location.getOffsetParent(this.__iframe.getContentElement().getDomElement()), "scroll"); - let divPos = qx.bom.element.Location.get(this.getContentElement().getDomElement(), "scroll"); - let divSize = qx.bom.element.Dimension.getSize(this.getContentElement().getDomElement()); + const domElement = this.getContentElement().getDomElement(); + if (domElement === null) { + return; + } + let divPos = qx.bom.element.Location.get(domElement, "scroll"); + let divSize = qx.bom.element.Dimension.getSize(domElement); this.__iframe.setLayoutProperties({ top: divPos.top - iframeParentPos.top, left: (divPos.left - iframeParentPos.left) @@ -139,6 +167,10 @@ qx.Class.define("osparc.component.widget.PersistentIframe", { width: (divSize.width), height: (divSize.height) }); + this.__restartButton.setLayoutProperties({ + top: (divPos.top - iframeParentPos.top), + right: (iframeParentPos.right - iframeParentPos.left - divPos.right) + 35 + }); this.__actionButton.setLayoutProperties({ top: (divPos.top - iframeParentPos.top), right: (iframeParentPos.right - iframeParentPos.left - divPos.right) diff --git a/services/web/client/source/class/osparc/component/workbench/NodeUI.js b/services/web/client/source/class/osparc/component/workbench/NodeUI.js index 2d839f136de..4a047c0b726 100644 --- a/services/web/client/source/class/osparc/component/workbench/NodeUI.js +++ b/services/web/client/source/class/osparc/component/workbench/NodeUI.js @@ -184,10 +184,8 @@ qx.Class.define("osparc.component.workbench.NodeUI", { this.setIcon("@FontAwesome5Solid/folder-open/14"); } const metaData = node.getMetaData(); - if (metaData) { - this.__createUIPorts(true, metaData.inputs); - this.__createUIPorts(false, metaData.outputs); - } + this.__createUIPorts(true, metaData && metaData.inputs); + this.__createUIPorts(false, metaData && metaData.outputs); if (node.isComputational() || node.isFilePicker()) { node.bind("progress", this.__progressBar, "value"); } diff --git a/services/web/client/source/class/osparc/component/workbench/ServiceCatalog.js b/services/web/client/source/class/osparc/component/workbench/ServiceCatalog.js index 1d569cc572b..cbf702eb86e 100644 --- a/services/web/client/source/class/osparc/component/workbench/ServiceCatalog.js +++ b/services/web/client/source/class/osparc/component/workbench/ServiceCatalog.js @@ -200,15 +200,10 @@ qx.Class.define("osparc.component.workbench.ServiceCatalog", { __populateList: function(reload = false) { this.__allServicesList = []; let store = osparc.store.Store.getInstance(); - let services = store.getServices(reload); - if (services === null) { - store.addListener("servicesRegistered", e => { - const data = e.getData(); - this.__addNewData(data["services"]); - }, this); - } else { - this.__addNewData(services); - } + store.getServices(reload) + .then(services => { + this.__addNewData(services); + }); }, __addNewData: function(newData) { diff --git a/services/web/client/source/class/osparc/data/Converters.js b/services/web/client/source/class/osparc/data/Converters.js index c0a1ad2ecab..20320fee639 100644 --- a/services/web/client/source/class/osparc/data/Converters.js +++ b/services/web/client/source/class/osparc/data/Converters.js @@ -239,6 +239,20 @@ qx.Class.define("osparc.data.Converters", { return "@MaterialIcons/insert_drive_file/15"; } return "@MaterialIcons/arrow_right_alt/15"; + }, + + replaceUuids: function(workbench) { + let workbenchStr = JSON.stringify(workbench); + const innerNodeIds = Object.keys(workbench); + for (let i=0; i { + this.startDynamicService(); + }) + .catch(err => { + const errorMsg = "Error when starting " + key + ":" + version + ": " + err.getTarget().getResponse()["error"]; + const errorMsgData = { + nodeId: this.getNodeId(), + msg: errorMsg + }; + this.fireDataEvent("showInLogger", errorMsgData); + this.setInteractiveStatus("failed"); + osparc.component.message.FlashMessenger.getInstance().logAs(this.tr("There was an error while starting the node."), "ERROR"); + }); + }, + + stopInBackend: function() { + // remove node in the backend + const study = osparc.store.Store.getInstance().getCurrentStudy(); + const params = { + url: { + projectId: study.getUuid(), + nodeId: this.getNodeId() + } + }; + osparc.data.Resources.fetch("studies", "deleteNode", params) + .catch(err => console.error(err)); + }, + repopulateOutputPortData: function() { if (this.__outputWidget) { this.__outputWidget.populatePortsData(); @@ -443,34 +495,50 @@ qx.Class.define("osparc.data.model.Node", { * */ __addSettings: function(inputs) { - const form = this.__settingsForm = new osparc.component.form.Auto(inputs, this); - form.addListener("linkAdded", e => { - const changedField = e.getData(); - this.getPropsWidget().linkAdded(changedField); - }, this); - form.addListener("linkRemoved", e => { - const changedField = e.getData(); - this.getPropsWidget().linkRemoved(changedField); - }, this); - + const form = this.__settingsForm = new osparc.component.form.Auto(inputs); const propsWidget = new osparc.component.form.renderer.PropForm(form, this); this.setPropsWidget(propsWidget); - propsWidget.addListener("removeLink", e => { - const changedField = e.getData(); - this.__settingsForm.removeLink(changedField); - }, this); - propsWidget.addListener("dataFieldModified", e => { - const portId = e.getData(); + propsWidget.addListener("linkModified", e => { + const linkModified = e.getData(); + const portId = linkModified.portId; this.__retrieveInputs(portId); }, this); }, + __addSettingsEditor: function(inputs) { + const propsWidget = this.getPropsWidget(); + const form = new osparc.component.form.Auto(inputs); + form.setData(this.__settingsForm.getData()); + const propsWidgetEditor = new osparc.component.form.renderer.PropFormEditor(form, this); + this.__settingsForm.addListener("changeData", e => { + // apply data + const data = this.__settingsForm.getData(); + form.setData(data); + }, this); + propsWidget.addListener("linkModified", e => { + const linkModified = e.getData(); + const portId = linkModified.portId; + const added = linkModified.added; + if (added) { + const srcControlLink = propsWidget.getControlLink(portId); + const controlLink = new qx.ui.form.TextField().set({ + enabled: false + }); + srcControlLink.bind("value", controlLink, "value"); + propsWidgetEditor.linkAdded(portId, controlLink); + } else { + propsWidgetEditor.linkRemoved(portId); + } + }, this); + this.setPropsWidgetEditor(propsWidgetEditor); + }, + removeNodePortConnections: function(inputNodeId) { let inputs = this.getInputValues(); for (const portId in inputs) { if (inputs[portId] && Object.prototype.hasOwnProperty.call(inputs[portId], "nodeUuid")) { if (inputs[portId]["nodeUuid"] === inputNodeId) { - this.__settingsForm.removeLink(portId); + this.getPropsWidget().removeLink(portId); } } } @@ -500,7 +568,10 @@ qx.Class.define("osparc.data.model.Node", { let filteredInputs = this.__removeNonSettingInputs(inputs); filteredInputs = this.__addMapper(filteredInputs); - this.__addSettings(filteredInputs); + if (Object.keys(filteredInputs).length) { + this.__addSettings(filteredInputs); + this.__addSettingsEditor(filteredInputs); + } }, __addOutputs: function(outputs) { @@ -509,23 +580,45 @@ qx.Class.define("osparc.data.model.Node", { this.__addOutputWidget(); }, - setInputData: function(nodeData) { - if (this.__settingsForm && nodeData) { - this.__settingsForm.setData(nodeData.inputs); - if ("inputAccess" in nodeData) { - this.__settingsForm.setAccessLevel(nodeData.inputAccess); - this.setInputAccess(nodeData.inputAccess); + __isInputDataALink: function(data) { + if (data !== null && typeof data === "object" && data.nodeUuid) { + return true; + } + return false; + }, + + setInputData: function(inputs) { + if (this.__settingsForm && inputs) { + const inputData = {}; + const inputLinks = {}; + const inputsCopy = osparc.utils.Utils.deepCloneObject(inputs); + for (let key in inputsCopy) { + if (this.__isInputDataALink(inputsCopy[key])) { + inputLinks[key] = inputsCopy[key]; + } else { + inputData[key] = inputsCopy[key]; + } } + this.getPropsWidget().addLinks(inputLinks); + this.__settingsForm.setData(inputData); + } + }, + + setInputDataAccess: function(inputAccess) { + if (inputAccess) { + this.setInputAccess(inputAccess); + this.getPropsWidget().setAccessLevel(inputAccess); + this.getPropsWidgetEditor().setAccessLevel(inputAccess); } }, - setOutputData: function(nodeData) { - if (nodeData.outputs) { - for (const outputKey in nodeData.outputs) { + setOutputData: function(outputs) { + if (outputs) { + for (const outputKey in outputs) { if (!Object.prototype.hasOwnProperty.call(this.__outputs, outputKey)) { this.__outputs[outputKey] = {}; } - this.__outputs[outputKey]["value"] = nodeData.outputs[outputKey]; + this.__outputs[outputKey]["value"] = outputs[outputKey]; this.fireDataEvent("outputChanged", outputKey); } } @@ -577,7 +670,7 @@ qx.Class.define("osparc.data.model.Node", { }, addPortLink: function(toPortId, fromNodeId, fromPortId) { - return this.__settingsForm.addLink(toPortId, fromNodeId, fromPortId); + return this.getPropsWidget().addLink(toPortId, fromNodeId, fromPortId); }, // ----- Input Nodes ----- @@ -587,9 +680,9 @@ qx.Class.define("osparc.data.model.Node", { addInputNodes: function(inputNodes) { if (inputNodes) { - for (let i=0; i { + this.addInputNode(inputNode); + }); } }, @@ -624,9 +717,9 @@ qx.Class.define("osparc.data.model.Node", { addOutputNodes: function(outputNodes) { if (outputNodes) { - for (let i=0; i { + this.addOutputNode(outputNode); + }); } }, @@ -665,7 +758,11 @@ qx.Class.define("osparc.data.model.Node", { restartIFrame: function(loadThis) { if (this.getIFrame() === null) { - this.setIFrame(new osparc.component.widget.PersistentIframe()); + const iframe = new osparc.component.widget.PersistentIframe(); + iframe.addListener("restart", () => { + this.restartIFrame(); + }, this); + this.setIFrame(iframe); } if (loadThis) { this.getIFrame().resetSource(); @@ -708,11 +805,22 @@ qx.Class.define("osparc.data.model.Node", { }, __retrieveInputs: function(portKey) { - const data = { - node: this, - portKey - }; - this.fireDataEvent("retrieveInputs", data); + if (this.isContainer()) { + const innerNodes = Object.values(this.getInnerNodes()); + for (let i=0; i { + let resp = e.getTarget().getResponse(); + if (typeof resp === "string") { + resp = JSON.parse(resp); + } const { data - } = e.getTarget().getResponse(); + } = resp; const sizeBytes = (data && ("size_bytes" in data)) ? data["size_bytes"] : 0; this.getPropsWidget().retrievedPortData(portKey, true, sizeBytes); console.log(data); @@ -764,22 +876,26 @@ qx.Class.define("osparc.data.model.Node", { addDynamicButtons: function() { if (this.isDynamic() && this.isRealService()) { - const retrieveBtn = new qx.ui.toolbar.Button(this.tr("Retrieve"), "@FontAwesome5Solid/spinner/14"); - osparc.utils.Utils.setIdToWidget(retrieveBtn, "nodeViewRetrieveBtn"); - retrieveBtn.addListener("execute", e => { - this.__retrieveInputs(); - }, this); - retrieveBtn.setEnabled(false); - this.setRetrieveIFrameButton(retrieveBtn); - - const restartBtn = new qx.ui.toolbar.Button(this.tr("Restart"), "@FontAwesome5Solid/redo-alt/14"); - osparc.utils.Utils.setIdToWidget(restartBtn, "nodeViewRestartBtn"); - restartBtn.addListener("execute", e => { - this.restartIFrame(); - }, this); - restartBtn.setEnabled(false); - this.setRestartIFrameButton(restartBtn); + this.__addRetrieveButton(); + this.__showLoadingIFrame(); } + if (this.isContainer()) { + const innerNodes = Object.values(this.getInnerNodes()); + if (innerNodes.some(innerNode => innerNode.isDynamic())) { + this.__addRetrieveButton(); + this.getRetrieveIFrameButton().setEnabled(true); + } + } + }, + + __addRetrieveButton: function() { + const retrieveBtn = new qx.ui.toolbar.Button(this.tr("Retrieve"), "@FontAwesome5Solid/spinner/14"); + osparc.utils.Utils.setIdToWidget(retrieveBtn, "nodeViewRetrieveBtn"); + retrieveBtn.addListener("execute", e => { + this.__retrieveInputs(); + }, this); + retrieveBtn.setEnabled(false); + this.setRetrieveIFrameButton(retrieveBtn); }, startDynamicService: function() { @@ -849,6 +965,10 @@ qx.Class.define("osparc.data.model.Node", { }, __nodeState: function() { const study = osparc.store.Store.getInstance().getCurrentStudy(); + if (study === null) { + return; + } + const params = { url: { projectId: study.getUuid(), @@ -898,7 +1018,6 @@ qx.Class.define("osparc.data.model.Node", { this.fireDataEvent("showInLogger", msgData); this.getRetrieveIFrameButton().setEnabled(true); - this.getRestartIFrameButton().setEnabled(true); this.setProgress(100); // FIXME: Apparently no all services are inmediately ready when they publish the port @@ -910,20 +1029,29 @@ qx.Class.define("osparc.data.model.Node", { this.__retrieveInputs(); }, - removeNode: function() { - this.removeIFrame(); + __removeInnerNodes: function() { const innerNodes = Object.values(this.getInnerNodes()); - for (const innerNode of innerNodes) { - innerNode.removeNode(); + for (let i=0; i { - node.startDynamicService(); - }) - .catch(err => { - const errorMsg = "Error when starting " + metaData.key + ":" + metaData.version + ": " + err.getTarget().getResponse()["error"]; - const errorMsgData = { - nodeId: node.getNodeId(), - msg: errorMsg - }; - node.fireDataEvent("showInLogger", errorMsgData); - node.setInteractiveStatus("failed"); - osparc.component.message.FlashMessenger.getInstance().logAs(this.tr("There was an error while starting the node."), "ERROR"); - }); + const metaData = node.getMetaData(); + if (metaData && Object.prototype.hasOwnProperty.call(metaData, "workbench")) { + this.__createInnerWorkbench(node, metaData); + } return node; }, + __createInnerWorkbench: function(parentNode, metaData) { + // this is must be a nodes group + const workbench = osparc.data.Converters.replaceUuids(metaData["workbench"]); + for (let innerNodeId in workbench) { + workbench[innerNodeId]["parent"] = workbench[innerNodeId]["parent"] || parentNode.getNodeId(); + } + + this.__deserializeWorkbench(workbench); + + for (let innerNodeId in workbench) { + this.getNode(innerNodeId).startInBackend(); + } + }, + __initNodeSignals: function(node) { if (node) { node.addListener("showInLogger", e => { @@ -247,10 +233,7 @@ qx.Class.define("osparc.data.model.Workbench", { const parentNode = this.getNode(nodeToClone.getParentNodeId()); let node = this.createNode(key, version, null, parentNode); const nodeData = nodeToClone.serialize(); - node.setInputData(nodeData); - node.setOutputData(nodeData); - node.addInputNodes(nodeData.inputNodes); - node.addOutputNodes(nodeData.outputNodes); + node.populateInputOutputData(nodeData); return node; }, @@ -284,16 +267,6 @@ qx.Class.define("osparc.data.model.Workbench", { if (!osparc.data.Permissions.getInstance().canDo("study.node.delete", true)) { return false; } - // remove node in the backend - const study = osparc.store.Store.getInstance().getCurrentStudy(); - const params = { - url: { - projectId: study.getUuid(), - nodeId: nodeId - } - }; - osparc.data.Resources.fetch("studies", "deleteNode", params) - .catch(err => console.error(err)); // remove first the connected edges const connectedEdges = this.getConnectedEdges(nodeId); @@ -480,7 +453,7 @@ qx.Class.define("osparc.data.model.Workbench", { const brotherNodes = this.__getBrotherNodes(currentModel, selectedNodeIds); // Create nodesGroup - const nodesGroupService = osparc.utils.Services.getNodesGroupService(); + const nodesGroupService = osparc.utils.Services.getNodesGroup(); const parentNode = currentModel.getNodeId ? currentModel : null; const nodesGroup = this.createNode(nodesGroupService.key, nodesGroupService.version, null, parentNode); if (!nodesGroup) { diff --git a/services/web/client/source/class/osparc/desktop/MainPage.js b/services/web/client/source/class/osparc/desktop/MainPage.js index e7dd48ee469..f564a7cc81f 100644 --- a/services/web/client/source/class/osparc/desktop/MainPage.js +++ b/services/web/client/source/class/osparc/desktop/MainPage.js @@ -89,7 +89,7 @@ qx.Class.define("osparc.desktop.MainPage", { let dashboard = this.__dashboard = new osparc.desktop.Dashboard(); dashboard.getStudyBrowser().addListener("startStudy", e => { const studyEditor = e.getData(); - this.__showStudyEditor(studyEditor); + this.__startStudyEditor(studyEditor); }, this); prjStack.add(dashboard); @@ -105,7 +105,7 @@ qx.Class.define("osparc.desktop.MainPage", { } }, - __showStudyEditor: function(studyEditor) { + __startStudyEditor: function(studyEditor) { if (this.__studyEditor) { this.__prjStack.remove(this.__studyEditor); } @@ -115,7 +115,7 @@ qx.Class.define("osparc.desktop.MainPage", { this.__prjStack.add(this.__studyEditor); this.__prjStack.setSelection([this.__studyEditor]); this.__navBar.setStudy(study); - this.__navBar.setPathButtons(study.getWorkbench().getPathIds(study.getUuid())); + this.__navBar.setPathButtons(this.__studyEditor.getCurrentPathIds()); this.__studyEditor.addListener("changeMainViewCaption", ev => { const elements = ev.getData(); diff --git a/services/web/client/source/class/osparc/desktop/NavigationBar.js b/services/web/client/source/class/osparc/desktop/NavigationBar.js index c1e9e93bdfe..56c43c95e89 100644 --- a/services/web/client/source/class/osparc/desktop/NavigationBar.js +++ b/services/web/client/source/class/osparc/desktop/NavigationBar.js @@ -152,20 +152,22 @@ qx.Class.define("osparc.desktop.NavigationBar", { setPathButtons: function(nodeIds) { this.__mainViewCaptionLayout.removeAll(); - const navBarLabelFont = qx.bom.Font.fromConfig(osparc.theme.Font.fonts["nav-bar-label"]); + nodeIds.length === 1 ? this.__studyTitle.show() : this.__studyTitle.exclude(); if (nodeIds.length === 0) { this.__highlightDashboard(true); - } else if (nodeIds.length === 1) { - this.__studyTitle.show(); return; } - this.__studyTitle.exclude(); + if (nodeIds.length === 1) { + return; + } + + const navBarLabelFont = qx.bom.Font.fromConfig(osparc.theme.Font.fonts["nav-bar-label"]); + const study = osparc.store.Store.getInstance().getCurrentStudy(); for (let i=0; i { - if (this.__servicesReady) { - userTimer.stop(); - this._removeAll(); - iframe.dispose(); - this.__createServicesLayout(); - this.__attachEventHandlers(); - } - }, this); - userTimer.start(); - - this.__initResources(); + this.__initResources(iframe); }, members: { - __servicesReady: null, + __reloadBtn: null, __serviceFilters: null, __allServices: null, - __servicesList: null, - __versionsList: null, - __searchTextfield: null, + __latestServicesModel: null, + __servicesUIList: null, + __versionsUIBox: null, + __deleteServiceBtn: null, + __selectedService: null, /** * Function that resets the selected item by reseting the filters and the service selection @@ -80,23 +69,42 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { if (this.__serviceFilters) { this.__serviceFilters.reset(); } - if (this.__servicesList) { - this.__servicesList.setSelection([]); + if (this.__servicesUIList) { + this.__servicesUIList.setSelection([]); } }, - __initResources: function() { - this.__getServicesPreload(); + __initResources: function(iframe) { + const store = osparc.store.Store.getInstance(); + store.getServices(true) + .then(services => { + // Do not validate if are not taking actions + // this.__nodeCheck(services); + this._removeAll(); + iframe.dispose(); + this.__createServicesLayout(); + this.__populateList(false); + this.__attachEventHandlers(); + }); }, - __getServicesPreload: function() { + __populateList: function(reload) { + this.__reloadBtn.setFetching(true); + const store = osparc.store.Store.getInstance(); - store.addListener("servicesRegistered", e => { - // Do not validate if are not taking actions - // this.__nodeCheck(e.getData()); - this.__servicesReady = e.getData(); - }, this); - store.getServices(true); + store.getServices(reload) + .then(services => { + this.__allServices = services; + this.__latestServicesModel.removeAll(); + for (const serviceKey in services) { + const latestService = osparc.utils.Services.getLatest(services, serviceKey); + this.__latestServicesModel.append(qx.data.marshal.Json.createModel(latestService)); + } + }) + .finally(() => { + this.__reloadBtn.setFetching(false); + this.__serviceFilters.dispatch(); + }); }, __createServicesLayout: function() { @@ -112,42 +120,47 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { __createServicesListLayout: function() { const servicesLayout = this.__createVBoxWLabel(this.tr("Services")); + // button for refetching services + const reloadBtn = this.__reloadBtn = new osparc.ui.form.FetchButton().set({ + label: this.tr("Reload"), + icon: "@FontAwesome5Solid/sync-alt/14", + allowGrowX: false + }); + reloadBtn.addListener("execute", function() { + this.__populateList(true); + }, this); + servicesLayout.add(reloadBtn); + const serviceFilters = this.__serviceFilters = new osparc.component.filter.group.ServiceFilterGroup("serviceBrowser"); servicesLayout.add(serviceFilters); - const servicesList = this.__servicesList = new qx.ui.form.List().set({ + const servicesUIList = this.__servicesUIList = new qx.ui.form.List().set({ orientation: "vertical", minWidth: 400, appearance: "pb-list" }); - servicesList.addListener("changeSelection", e => { + servicesUIList.addListener("changeSelection", e => { if (e.getData() && e.getData().length>0) { const selectedKey = e.getData()[0].getModel(); this.__serviceSelected(selectedKey); } }, this); - const store = osparc.store.Store.getInstance(); - const latestServices = []; - const services = this.__allServices = store.getServices(); - for (const serviceKey in services) { - latestServices.push(osparc.utils.Services.getLatest(services, serviceKey)); - } - const latestServicesModel = new qx.data.Array( - latestServices.map(s => qx.data.marshal.Json.createModel(s)) - ); - const servCtrl = new qx.data.controller.List(latestServicesModel, servicesList, "name"); + + const latestServicesModel = this.__latestServicesModel = new qx.data.Array(); + const servCtrl = new qx.data.controller.List(latestServicesModel, servicesUIList, "name"); servCtrl.setDelegate({ createItem: () => { const item = new osparc.desktop.ServiceBrowserListItem(); item.subscribeToFilterGroup("serviceBrowser"); item.addListener("tap", e => { - servicesList.setSelection([item]); + servicesUIList.setSelection([item]); }); return item; }, bindItem: (ctrl, item, id) => { ctrl.bindProperty("key", "model", null, item, id); ctrl.bindProperty("key", "key", null, item, id); + ctrl.bindProperty("version", "version", null, item, id); ctrl.bindProperty("name", "title", null, item, id); ctrl.bindProperty("description", "description", null, item, id); ctrl.bindProperty("type", "type", null, item, id); @@ -155,20 +168,10 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { ctrl.bindProperty("contact", "contact", null, item, id); } }); - servicesLayout.add(servicesList, { + servicesLayout.add(servicesUIList, { flex: 1 }); - // Workaround to the list.changeSelection - servCtrl.addListener("changeValue", e => { - if (e.getData() && e.getData().length>0) { - const selectedService = e.getData().toArray()[0]; - this.__serviceSelected(selectedService); - } else { - this.__serviceSelected(null); - } - }, this); - return servicesLayout; }, @@ -187,7 +190,7 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { titleContainer.add(new qx.ui.basic.Atom(this.tr("Version"))); - const versions = this.__versionsList = new qx.ui.form.SelectBox(); + const versions = this.__versionsUIBox = new qx.ui.form.SelectBox(); osparc.utils.Utils.setIdToWidget(versions, "serviceBrowserVersionsDrpDwn"); titleContainer.add(versions); versions.addListener("changeSelection", e => { @@ -197,6 +200,26 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { }, this); descriptionView.add(titleContainer); + const actionsContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + actionsContainer.add(new qx.ui.core.Spacer(300, null)); + const deleteServiceBtn = this.__deleteServiceBtn = new osparc.ui.form.FetchButton(this.tr("Delete")).set({ + allowGrowX: false, + visibility: "hidden" + }); + deleteServiceBtn.addListener("execute", () => { + const msg = this.tr("Are you sure you want to delete the group?"); + const win = new osparc.ui.window.Confirmation(msg); + win.addListener("close", () => { + if (win.getConfirmed()) { + this.__deleteService(); + } + }, this); + win.center(); + win.open(); + }, this); + actionsContainer.add(deleteServiceBtn); + descriptionView.add(actionsContainer); + const descriptionContainer = this.__serviceDescription = new qx.ui.container.Scroll(); descriptionView.add(descriptionContainer, { flex: 1 @@ -226,17 +249,17 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { }, this); textfield.addListener("keypress", e => { if (e.getKeyIdentifier() === "Enter") { - const selectables = this.__servicesList.getSelectables(); + const selectables = this.__servicesUIList.getSelectables(); if (selectables) { - this.__servicesList.setSelection([selectables[0]]); + this.__servicesUIList.setSelection([selectables[0]]); } } }, this); }, __serviceSelected: function(serviceKey) { - if (this.__versionsList) { - const versionsList = this.__versionsList; + if (this.__versionsUIBox) { + const versionsList = this.__versionsUIBox; versionsList.removeAll(); if (serviceKey in this.__allServices) { const versions = osparc.utils.Services.getVersions(this.__allServices, serviceKey); @@ -258,7 +281,7 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { }, __versionSelected: function(versionKey) { - const serviceSelection = this.__servicesList.getSelection(); + const serviceSelection = this.__servicesUIList.getSelection(); if (serviceSelection.length > 0) { const serviceKey = serviceSelection[0].getModel(); const selectedService = osparc.utils.Services.getFromObject(this.__allServices, serviceKey, versionKey); @@ -267,15 +290,47 @@ qx.Class.define("osparc.desktop.ServiceBrowser", { }, __updateServiceDescription: function(selectedService) { + let showDelete = false; const serviceDescription = this.__serviceDescription; if (serviceDescription) { - if (selectedService) { - const serviceInfo = new osparc.component.metadata.ServiceInfo(selectedService); - serviceDescription.add(serviceInfo); - } else { - serviceDescription.add(null); - } + const serviceInfo = selectedService ? new osparc.component.metadata.ServiceInfo(selectedService) : null; + serviceDescription.add(serviceInfo); + this.__selectedService = selectedService; + showDelete = this.__canServiceBeDeleted(selectedService); + } + this.__deleteServiceBtn.setVisibility(showDelete ? "visible" : "hidden"); + }, + + __canServiceBeDeleted: function(selectedService) { + if (selectedService) { + const isMacro = selectedService.key.includes("frontend/nodes-group/macros"); + const isOwner = selectedService.contact === osparc.auth.Data.getInstance().getEmail(); + return isMacro && isOwner; } + return false; + }, + + __deleteService: function() { + this.__deleteServiceBtn.setFetching(true); + + const serviceId = this.__selectedService.id; + const params = { + url: { + groupId: serviceId + } + }; + osparc.data.Resources.fetch("groups", "delete", params, serviceId) + .then(() => { + this.__updateServiceDescription(null); + this.__populateList(true); + }) + .catch(err => { + osparc.component.message.FlashMessenger.getInstance().logAs(this.tr("Unable to delete the group."), "ERROR"); + console.error(err); + }) + .finally(() => { + this.__deleteServiceBtn.setFetching(false); + }); }, __nodeCheck: function(services) { diff --git a/services/web/client/source/class/osparc/desktop/ServiceBrowserListItem.js b/services/web/client/source/class/osparc/desktop/ServiceBrowserListItem.js index d005d8c6eed..38f192ce30b 100644 --- a/services/web/client/source/class/osparc/desktop/ServiceBrowserListItem.js +++ b/services/web/client/source/class/osparc/desktop/ServiceBrowserListItem.js @@ -74,6 +74,15 @@ qx.Class.define("osparc.desktop.ServiceBrowserListItem", { apply : "_applyKey" }, + version: { + check: "String" + }, + + dagId: { + check : "String", + nullable : true + }, + title: { check : "String", apply : "_applyTitle", @@ -161,22 +170,34 @@ qx.Class.define("osparc.desktop.ServiceBrowserListItem", { }, _applyKey: function(value, old) { + if (value === null) { + return; + } const parts = value.split("/"); const id = parts.pop(); osparc.utils.Utils.setIdToWidget(this, "serviceBrowserListItem_"+id); }, _applyTitle: function(value) { + if (value === null) { + return; + } const label = this.getChildControl("title"); label.setValue(value); }, _applyDescription: function(value) { + if (value === null) { + return; + } const label = this.getChildControl("description"); label.setValue(value); }, _applyContact: function(value) { + if (value === null) { + return; + } const label = this.getChildControl("contact"); label.setValue(value); }, @@ -193,7 +214,7 @@ qx.Class.define("osparc.desktop.ServiceBrowserListItem", { }, _shouldApplyFilter: function(data) { - if (data.text) { + if (data.text && this.getTitle()) { const label = this.getTitle() .trim() .toLowerCase(); @@ -201,7 +222,7 @@ qx.Class.define("osparc.desktop.ServiceBrowserListItem", { return true; } } - if (data.tags && data.tags.length) { + if (data.tags && data.tags.length && this.getCategory()) { const category = this.getCategory() || ""; const type = this.getType() || ""; if (!data.tags.includes(osparc.utils.Utils.capitalize(category.trim())) && !data.tags.includes(osparc.utils.Utils.capitalize(type.trim()))) { diff --git a/services/web/client/source/class/osparc/desktop/StudyBrowser.js b/services/web/client/source/class/osparc/desktop/StudyBrowser.js index d7b88a81fd3..29c56b766bd 100644 --- a/services/web/client/source/class/osparc/desktop/StudyBrowser.js +++ b/services/web/client/source/class/osparc/desktop/StudyBrowser.js @@ -204,9 +204,8 @@ qx.Class.define("osparc.desktop.StudyBrowser", { __getServicesPreload: function() { let store = osparc.store.Store.getInstance(); store.addListener("servicesRegistered", e => { - this.__servicesReady = e.getData(); + this.__servicesReady = true; }, this); - store.getServices(true); }, __createStudiesLayout: function() { @@ -293,7 +292,7 @@ qx.Class.define("osparc.desktop.StudyBrowser", { win.center(); win.open(); win.addListener("close", () => { - if (win["value"] === 1) { + if (win.getConfirmed()) { this.__deleteStudy(selection.map(button => this.__getStudyData(button.getUuid(), isTemplate)), isTemplate); } }, this); @@ -580,18 +579,8 @@ qx.Class.define("osparc.desktop.StudyBrowser", { }, __createConfirmWindow: function(isMulti) { - const win = new osparc.ui.window.Dialog("Confirmation", null, - `Are you sure you want to delete the ${isMulti ? "studies" : "study"}?` - ); - const btnYes = new qx.ui.toolbar.Button("Yes"); - osparc.utils.Utils.setIdToWidget(btnYes, "confirmDeleteStudyBtn"); - btnYes.addListener("execute", e => { - win["value"] = 1; - win.close(1); - }, this); - win.addCancelButton(); - win.addButton(btnYes); - return win; + const msg = isMulti ? this.tr("Are you sure you want to delete the studies?") : this.tr("Are you sure you want to delete the study?"); + return new osparc.ui.window.Confirmation(msg); } } }); diff --git a/services/web/client/source/class/osparc/desktop/StudyBrowserListItem.js b/services/web/client/source/class/osparc/desktop/StudyBrowserListItem.js index 332dc65861b..9c74ed933ca 100644 --- a/services/web/client/source/class/osparc/desktop/StudyBrowserListItem.js +++ b/services/web/client/source/class/osparc/desktop/StudyBrowserListItem.js @@ -149,7 +149,7 @@ qx.Class.define("osparc.desktop.StudyBrowserListItem", { return control || this.base(arguments, id); }, - // overriden + // overridden _applyUuid: function(value, old) { osparc.utils.Utils.setIdToWidget(this, "studyBrowserListItem_"+value); }, diff --git a/services/web/client/source/class/osparc/desktop/StudyEditor.js b/services/web/client/source/class/osparc/desktop/StudyEditor.js index 4798e4bbebf..429b3224b19 100644 --- a/services/web/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/web/client/source/class/osparc/desktop/StudyEditor.js @@ -69,6 +69,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { __loggerView: null, __currentNodeId: null, __autoSaveTimer: null, + __lastSavedStudy: null, _applyStudy: function(study) { osparc.store.Store.getInstance().setCurrentStudy(study); @@ -184,21 +185,28 @@ qx.Class.define("osparc.desktop.StudyEditor", { const nodeId = e.getData(); const node = this.getStudy().getWorkbench().getNode(nodeId); if (node && node.isContainer()) { - // const exportGroupView = new osparc.component.export.ExportGroup(node); - + const exportGroupView = new osparc.component.export.ExportGroup(node); const window = new qx.ui.window.Window(this.tr("Export: ") + node.getLabel()).set({ appearance: "service-window", layout: new qx.ui.layout.Grow(), autoDestroy: true, contentPadding: 0, - width: 900, - height: 800, + width: 700, + height: 700, showMinimize: false, modal: true }); - // window.add(exportGroupView); + window.add(exportGroupView); window.center(); window.open(); + + window.addListener("close", () => { + exportGroupView.tearDown(); + }, this); + + exportGroupView.addListener("finished", () => { + window.close(); + }, this); } }); @@ -340,6 +348,11 @@ qx.Class.define("osparc.desktop.StudyEditor", { this.fireDataEvent("changeMainViewCaption", nodesPath); }, + getCurrentPathIds: function() { + const nodesPath = this.getStudy().getWorkbench().getPathIds(this.__currentNodeId); + return nodesPath; + }, + getLogger: function() { return this.__loggerView; }, @@ -461,6 +474,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { __doStartPipeline: function() { this.getStudy().getWorkbench().clearProgressData(); + // post pipeline const url = "/computation/pipeline/" + encodeURIComponent(this.getStudy().getUuid()) + "/start"; const req = new osparc.io.request.ApiRequest(url, "POST"); @@ -524,7 +538,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { let timer = this.__autoSaveTimer = new qx.event.Timer(interval); timer.addListener("interval", () => { const newObj = this.getStudy().serializeStudy(); - const delta = diffPatcher.diff(this.__lastSavedPrj, newObj); + const delta = diffPatcher.diff(this.__lastSavedStudy, newObj); if (delta) { let deltaKeys = Object.keys(delta); // lastChangeDate should not be taken into account as data change @@ -561,7 +575,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { }; osparc.data.Resources.fetch("studies", "put", params).then(data => { this.fireDataEvent("studySaved", true); - this.__lastSavedPrj = osparc.wrapper.JsonDiffPatch.getInstance().clone(newObj); + this.__lastSavedStudy = osparc.wrapper.JsonDiffPatch.getInstance().clone(newObj); if (cbSuccess) { cbSuccess.call(this); } diff --git a/services/web/client/source/class/osparc/dev/fake/Data.js b/services/web/client/source/class/osparc/dev/fake/Data.js index ded5f7edcba..7f42b8d83ab 100644 --- a/services/web/client/source/class/osparc/dev/fake/Data.js +++ b/services/web/client/source/class/osparc/dev/fake/Data.js @@ -25,156 +25,6 @@ qx.Class.define("osparc.dev.fake.Data", { type: "static", statics: { - getFakeServices: function() { - return [{ - key: "simcore/services/computational/itis/sleeper", - version: "0.0.0", - type: "computational", - name: "sleeper service", - description: "dummy sleepr service", - authors: [ - { - name: "Odei Maiz", - email: "maiz@itis.ethz.ch" - } - ], - contact: "maiz@itis.ethz.ch", - inputs: { - inNumber: { - displayOrder: 0, - label: "In", - description: "Chosen Number", - type: "number", - defaultValue: 42 - } - }, - outputs: { - outNumber: { - displayOrder: 0, - label: "Out", - description: "Chosen Number", - type: "number" - } - } - }, { - key: "simcore/services/computational/itis/tutti", - version: "0.0.0", - type: "computational", - name: "a little test node", - description: "just the bare minimum", - authors: [ - { - name: "Tobias Oetiker", - email: "oetiker@itis.ethz.ch" - } - ], - contact: "oetiker@itis.ethz.ch", - inputs: { - inNumber: { - displayOrder: 0, - label: "Number Test", - description: "Test Input for Number", - type: "number", - defaultValue: 5.3 - }, - inInt: { - displayOrder: 1, - label: "Integer Test", - description: "Test Input for Integer", - type: "integer", - defaultValue: 2 - }, - inBool: { - displayOrder: 2, - label: "Boolean Test", - type: "boolean", - description: "Test Input for Boolean", - defaultValue: true - }, - inStr: { - displayOrder: 3, - type: "string", - label: "String Test", - description: "Test Input for String", - defaultValue: "Gugus" - }, - inArea: { - displayOrder: 4, - type: "string", - label: "Widget TextArea Test", - description: "Test Input for String", - defaultValue: "Gugus\nDu\nDa", - widget: { - type: "TextArea", - minHeight: 50 - } - }, - inSb: { - displayOrder: 5, - label: "Widget SelectBox Test", - description: "Test Input for SelectBox", - defaultValue: "dog", - type: "string", - widget: { - /* - type: "SelectBox", - structure: [ - { - key: "dog", - label: "A Dog" - }, - { - key: "cat", - label: "A Cat" - } - ] - */ - type: "TextArea", - minHeight: 50 - } - }, - inFile: { - displayOrder: 6, - label: "File", - description: "Test Input File", - type: "data:*/*" - }, - inImage: { - displayOrder: 7, - label: "Image", - description: "Test Input Image", - type: "data:[image/jpeg,image/png]" - } - }, - outputs: { - outNumber: { - label: "Number Test", - description: "Test Output for Number", - displayOrder: 0, - type: "number" - }, - outInteger: { - label: "Integer Test", - description: "Test Output for Integer", - displayOrder: 1, - type: "integer" - }, - outBool: { - label: "Boolean Test", - description: "Test Output for Boolean", - displayOrder: 2, - type: "boolean" - }, - outPng: { - label: "Png Test", - description: "Test Output for PNG Image", - displayOrder: 3, - type: "data:image/png" - } - } - }]; - }, - getItemList: function(nodeKey, portKey) { switch (portKey) { case "defaultNeuromanModels": diff --git a/services/web/client/source/class/osparc/store/Store.js b/services/web/client/source/class/osparc/store/Store.js index 8241a684542..6a6d70b4e4b 100644 --- a/services/web/client/source/class/osparc/store/Store.js +++ b/services/web/client/source/class/osparc/store/Store.js @@ -81,6 +81,10 @@ qx.Class.define("osparc.store.Store", { check: "Array", init: [] }, + groups: { + check: "Array", + init: [] + }, storageLocations: { check: "Array", init: [] @@ -155,33 +159,24 @@ qx.Class.define("osparc.store.Store", { * @param {Boolean} reload ? */ getServices: function(reload) { - if (!osparc.utils.Services.reloadingServices && (reload || Object.keys(osparc.utils.Services.servicesCached).length === 0)) { - osparc.utils.Services.reloadingServices = true; - osparc.data.Resources.get("servicesTodo", null, !reload) - .then(data => { - const allServices = data.concat(osparc.utils.Services.getBuiltInServices()); - const filteredServices = osparc.utils.Services.filterOutUnavailableGroups(allServices); - const services = osparc.utils.Services.convertArrayToObject(filteredServices); - osparc.utils.Services.servicesToCache(services, true); - this.fireDataEvent("servicesRegistered", { - services, - fromServer: true - }); + return new Promise((resolve, reject) => { + const allServices = osparc.utils.Services.getBuiltInServices(); + const servicesPromise = osparc.data.Resources.get("servicesTodo", null, !reload); + const groupsPromise = osparc.data.Resources.get("groups", null, !reload); + Promise.all([servicesPromise, groupsPromise]) + .then(values => { + allServices.push(...values[0], ...values[1]); }) .catch(err => { console.error("getServices failed", err); - const allServices = osparc.dev.fake.Data.getFakeServices().concat(osparc.utils.Services.getBuiltInServices()); - const filteredServices = osparc.utils.Services.filterOutUnavailableGroups(allServices); - const services = osparc.utils.Services.convertArrayToObject(filteredServices); - osparc.utils.Services.servicesToCache(services, false); - this.fireDataEvent("servicesRegistered", { - services, - fromServer: false - }); + }) + .finally(() => { + const servicesObj = osparc.utils.Services.convertArrayToObject(allServices); + osparc.utils.Services.servicesToCache(servicesObj, true); + this.fireDataEvent("servicesRegistered", servicesObj); + resolve(osparc.utils.Services.servicesCached); }); - return null; - } - return osparc.utils.Services.servicesCached; + }); }, /** diff --git a/services/web/client/source/class/osparc/ui/window/Confirmation.js b/services/web/client/source/class/osparc/ui/window/Confirmation.js new file mode 100644 index 00000000000..714997f96a8 --- /dev/null +++ b/services/web/client/source/class/osparc/ui/window/Confirmation.js @@ -0,0 +1,39 @@ +/* + * oSPARC - The SIMCORE frontend - https://osparc.io + * Copyright: 2020 IT'IS Foundation - https://itis.swiss + * License: MIT - https://opensource.org/licenses/MIT + * Authors: Odei Maiz (odeimaiz) + */ + +/** + * Generic confirmation window. + * Provides "Cancel" and "Yes" buttons as well as boolean Confirmed property. + */ +qx.Class.define("osparc.ui.window.Confirmation", { + extend: osparc.ui.window.Dialog, + + /** + * @extends osparc.ui.window.Dialog + * @param {String} message Message that will be displayed to the user. + */ + construct: function(message) { + this.base(arguments, this.tr("Confirmation"), null, message); + + this.addCancelButton(); + + const btnYes = new qx.ui.toolbar.Button("Yes"); + osparc.utils.Utils.setIdToWidget(btnYes, "confirmDeleteStudyBtn"); + btnYes.addListener("execute", e => { + this.setConfirmed(true); + this.close(1); + }, this); + this.addButton(btnYes); + }, + + properties: { + confirmed: { + check: "Boolean", + init: false + } + } +}); diff --git a/services/web/client/source/class/osparc/utils/Services.js b/services/web/client/source/class/osparc/utils/Services.js index 87b5c0885f8..4a4803ec6d2 100644 --- a/services/web/client/source/class/osparc/utils/Services.js +++ b/services/web/client/source/class/osparc/utils/Services.js @@ -75,7 +75,6 @@ qx.Class.define("osparc.utils.Services", { } }, - reloadingServices: false, servicesCached: {}, getTypes: function() { @@ -166,48 +165,15 @@ qx.Class.define("osparc.utils.Services", { return false; }, - filterOutUnavailableGroups: function(listOfServices) { - const filteredServices = []; - for (let i=0; i>.env echo WEBSERVER_LOGIN_REGISTRATION_CONFIRMATION_REQUIRED=0 >>.env -echo WEBSERVER_DB_INITTABLES=1 >>.env # set max number of CPUs sidecar echo SERVICES_MAX_NANO_CPUS=2000000000 >> .env diff --git a/tests/e2e/tutorials/tutorialBase.js b/tests/e2e/tutorials/tutorialBase.js index d3744801a68..272493f7cf0 100644 --- a/tests/e2e/tutorials/tutorialBase.js +++ b/tests/e2e/tutorials/tutorialBase.js @@ -55,13 +55,39 @@ class TutorialBase { async login() { this.__responsesQueue.addResponseListener("projects?type=template"); + this.__responsesQueue.addResponseListener("catalog/dags"); + this.__responsesQueue.addResponseListener("services"); await auto.logIn(this.__page, this.__user, this.__pass); try { - await this.__responsesQueue.waitUntilResponse("projects?type=template"); + const resp = await this.__responsesQueue.waitUntilResponse("projects?type=template"); + const templates = resp["data"]; + console.log("Templates received", templates.length); + templates.forEach(template => { + console.log(" - ", template.name); + }); } catch(err) { console.error("Templates could not be fetched", err); } + try { + const resp = await this.__responsesQueue.waitUntilResponse("catalog/dags"); + const dags = resp["data"]; + console.log("DAGs received:", dags.length); + dags.forEach(dag => { + console.log(" - ", dag.name); + }); + } + catch(err) { + console.error("DAGs could not be fetched", err); + } + try { + const resp = await this.__responsesQueue.waitUntilResponse("services"); + const services = resp["data"]; + console.log("Services received:", services.length); + } + catch(err) { + console.error("Services could not be fetched", err); + } } async openTemplate(waitFor = 1000) { diff --git a/tests/e2e/utils/responsesQueue.js b/tests/e2e/utils/responsesQueue.js index 1366f6c0085..8f161468ab2 100644 --- a/tests/e2e/utils/responsesQueue.js +++ b/tests/e2e/utils/responsesQueue.js @@ -4,19 +4,20 @@ class ResponsesQueue { constructor(page) { this.__page = page; this.__reqQueue = []; - this.__respQueue = []; + this.__respPendingQueue = []; + this.__respReceivedQueue = {}; } addResponseListener(url) { const page = this.__page; const reqQueue = this.__reqQueue; - const respQueue = this.__respQueue; + const respPendingQueue = this.__respPendingQueue; reqQueue.push(url); - respQueue.push(url); + respPendingQueue.push(url); console.log("-- Expected response added to queue", url); page.on("request", function callback(req) { if (req.url().includes(url)) { - console.log((new Date).toUTCString(), "-- Queued request sent", req.url()); + console.log((new Date).toUTCString(), "-- Queued request sent", req.method(), req.url()); page.removeListener("request", callback); const index = reqQueue.indexOf(url); if (index > -1) { @@ -24,14 +25,18 @@ class ResponsesQueue { } } }); + const that = this; page.on("response", function callback(resp) { if (resp.url().includes(url)) { - console.log((new Date).toUTCString(), "-- Queued response received", resp.url()); - page.removeListener("response", callback); - const index = respQueue.indexOf(url); - if (index > -1) { - respQueue.splice(index, 1); - } + console.log((new Date).toUTCString(), "-- Queued response received", resp.url(), ":"); + resp.json().then(data => { + that.__respReceivedQueue[url] = data; + page.removeListener("response", callback); + const index = respPendingQueue.indexOf(url); + if (index > -1) { + respPendingQueue.splice(index, 1); + } + }); } }); } @@ -41,7 +46,7 @@ class ResponsesQueue { } isResponseInQueue(url) { - return this.__respQueue.includes(url); + return this.__respPendingQueue.includes(url); } async waitUntilResponse(url, timeout = 10000) { @@ -55,6 +60,16 @@ class ResponsesQueue { if (sleptFor >= timeout) { throw("-- Timeout reached." + new Date().toUTCString()); } + // console.log("waitUntilResponse", url); + // console.log(Object.keys(this.__respReceivedQueue)); + if (Object.prototype.hasOwnProperty.call(this.__respReceivedQueue, url)) { + const resp = this.__respReceivedQueue[url]; + if (resp && "error" in resp && resp["error"] !== null) { + throw("-- Error in response", resp["error"]); + } + delete this.__respReceivedQueue[url]; + return resp; + } } }