diff --git a/packages/pytest-simcore/src/pytest_simcore/__init__.py b/packages/pytest-simcore/src/pytest_simcore/__init__.py index 60638a8946e..8716d997ef2 100644 --- a/packages/pytest-simcore/src/pytest_simcore/__init__.py +++ b/packages/pytest-simcore/src/pytest_simcore/__init__.py @@ -3,6 +3,9 @@ import pytest +# NOTE: this ensures that assertion printouts are nicely formated and complete see https://lorepirri.com/pytest-register-assert-rewrite.html +pytest.register_assert_rewrite("pytest_simcore.helpers") + __version__: str = version("pytest-simcore") diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/__init__.py b/packages/pytest-simcore/src/pytest_simcore/helpers/__init__.py index d13e95469d2..4b993f36ef7 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/__init__.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/__init__.py @@ -1,8 +1,4 @@ # pytest_simcore.docker_compose fixture module config variables -import pytest FIXTURE_CONFIG_CORE_SERVICES_SELECTION = "pytest_simcore_core_services_selection" FIXTURE_CONFIG_OPS_SERVICES_SELECTION = "pytest_simcore_ops_services_selection" - -# NOTE: this ensures that assertion printouts are nicely formated and complete see https://lorepirri.com/pytest-register-assert-rewrite.html -pytest.register_assert_rewrite("pytest_simcore.helpers") diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 0980348e8bb..3316f4276ed 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -44,7 +44,7 @@ sqlalchemy<2.0 # SEE https://github.com/pytest-dev/pytest-asyncio/issues/706 # Many tests fail with `RuntimeError: There is no current event loop in thread 'MainThread'` -pytest-asyncio<0.22 +pytest-asyncio<0.24 # # Bugs diff --git a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py index bbe24ade928..6b3d1afea3d 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/dynamic_sidecar/docker_service_specs/proxy.py @@ -92,7 +92,7 @@ def get_dynamic_proxy_spec( f"traefik.http.routers.{scheduler_data.proxy_service_name}.entrypoints": "http", f"traefik.http.routers.{scheduler_data.proxy_service_name}.priority": "10", f"traefik.http.routers.{scheduler_data.proxy_service_name}.rule": f"hostregexp(`{scheduler_data.node_uuid}.services.{{host:.+}}`)", - f"traefik.http.routers.{scheduler_data.proxy_service_name}.middlewares": f"{dynamic_services_scheduler_settings.SWARM_STACK_NAME}_gzip@docker, {scheduler_data.proxy_service_name}-security-headers", + f"traefik.http.routers.{scheduler_data.proxy_service_name}.middlewares": f"{dynamic_services_scheduler_settings.SWARM_STACK_NAME}_gzip@swarm, {scheduler_data.proxy_service_name}-security-headers", "dynamic_type": "dynamic-sidecar", # tagged as dynamic service } | StandardSimcoreDockerLabels( diff --git a/services/director/src/simcore_service_director/producer.py b/services/director/src/simcore_service_director/producer.py index 20f34ae3608..b74da40c913 100644 --- a/services/director/src/simcore_service_director/producer.py +++ b/services/director/src/simcore_service_director/producer.py @@ -267,7 +267,7 @@ async def _create_docker_service_params( f"traefik.http.routers.{service_name}.rule": f"PathPrefix(`/x/{node_uuid}`)", f"traefik.http.routers.{service_name}.entrypoints": "http", f"traefik.http.routers.{service_name}.priority": "10", - f"traefik.http.routers.{service_name}.middlewares": f"{config.SWARM_STACK_NAME}_gzip@docker", + f"traefik.http.routers.{service_name}.middlewares": f"{config.SWARM_STACK_NAME}_gzip@swarm", }, "networks": [internal_network_id] if internal_network_id else [], } diff --git a/services/docker-compose-ops.yml b/services/docker-compose-ops.yml index ff4949f9c43..c5265e44d2a 100644 --- a/services/docker-compose-ops.yml +++ b/services/docker-compose-ops.yml @@ -75,14 +75,14 @@ services: environment: - >- REDIS_HOSTS= - resources:${REDIS_HOST}:${REDIS_PORT}:0, - locks:${REDIS_HOST}:${REDIS_PORT}:1, - validation_codes:${REDIS_HOST}:${REDIS_PORT}:2, - scheduled_maintenance:${REDIS_HOST}:${REDIS_PORT}:3, - user_notifications:${REDIS_HOST}:${REDIS_PORT}:4, - announcements:${REDIS_HOST}:${REDIS_PORT}:5, - distributed_identifiers:${REDIS_HOST}:${REDIS_PORT}:6, - deferred_tasks:${REDIS_HOST}:${REDIS_PORT}:7 + resources:${REDIS_HOST}:${REDIS_PORT}:0:${REDIS_PASSWORD}, + locks:${REDIS_HOST}:${REDIS_PORT}:1:${REDIS_PASSWORD}, + validation_codes:${REDIS_HOST}:${REDIS_PORT}:2:${REDIS_PASSWORD}, + scheduled_maintenance:${REDIS_HOST}:${REDIS_PORT}:3:${REDIS_PASSWORD}, + user_notifications:${REDIS_HOST}:${REDIS_PORT}:4:${REDIS_PASSWORD}, + announcements:${REDIS_HOST}:${REDIS_PORT}:5:${REDIS_PASSWORD}, + distributed_identifiers:${REDIS_HOST}:${REDIS_PORT}:6:${REDIS_PASSWORD}, + deferred_tasks:${REDIS_HOST}:${REDIS_PORT}:7:${REDIS_PASSWORD} # If you add/remove a db, do not forget to update the --databases entry in the docker-compose.yml ports: - "18081:8081" diff --git a/services/docker-compose.local.yml b/services/docker-compose.local.yml index 2ba2aa37a2d..d52eea00ed6 100644 --- a/services/docker-compose.local.yml +++ b/services/docker-compose.local.yml @@ -131,7 +131,7 @@ services: - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.entrypoints=http - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.rule=hostregexp(`{host:.+}`) && PathPrefix(`/dev/`) - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.priority=3 - - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.middlewares=${SWARM_STACK_NAME}_gzip@docker, ${SWARM_STACK_NAME_NO_HYPHEN}_sslheader@docker, ${SWARM_STACK_NAME}_webserver_retry + - traefik.http.routers.${SWARM_STACK_NAME}_webserver_local.middlewares=${SWARM_STACK_NAME}_gzip@swarm, ${SWARM_STACK_NAME_NO_HYPHEN}_sslheader@swarm, ${SWARM_STACK_NAME}_webserver_retry wb-api-server: environment: @@ -233,7 +233,7 @@ services: - traefik.http.routers.${SWARM_STACK_NAME}_api_internal.service=api@internal - traefik.http.routers.${SWARM_STACK_NAME}_api_internal.rule=PathPrefix(`/dashboard`) || PathPrefix(`/api`) - traefik.http.routers.${SWARM_STACK_NAME}_api_internal.entrypoints=traefik_monitor - - traefik.http.routers.${SWARM_STACK_NAME}_api_internal.middlewares=${SWARM_STACK_NAME}_gzip@docker + - traefik.http.routers.${SWARM_STACK_NAME}_api_internal.middlewares=${SWARM_STACK_NAME}_gzip@swarm - traefik.http.services.${SWARM_STACK_NAME}_api_internal.loadbalancer.server.port=8080 whoami: @@ -247,4 +247,4 @@ services: - traefik.http.services.${SWARM_STACK_NAME}_whoami.loadbalancer.server.port=80 - traefik.http.routers.${SWARM_STACK_NAME}_whoami.rule=PathPrefix(`/whoami`) - traefik.http.routers.${SWARM_STACK_NAME}_whoami.entrypoints=traefik_monitor - - traefik.http.routers.${SWARM_STACK_NAME}_whoami.middlewares=${SWARM_STACK_NAME}_gzip@docker + - traefik.http.routers.${SWARM_STACK_NAME}_whoami.middlewares=${SWARM_STACK_NAME}_gzip@swarm diff --git a/services/docker-compose.yml b/services/docker-compose.yml index 1564a1b3185..842b07d9c38 100644 --- a/services/docker-compose.yml +++ b/services/docker-compose.yml @@ -56,7 +56,7 @@ services: - traefik.http.routers.${SWARM_STACK_NAME}_api-server.rule=hostregexp(`{host:.+}`) && (Path(`/`, `/v0`) || PathPrefix(`/v0/`) || Path(`/api/v0/openapi.json`)) - traefik.http.routers.${SWARM_STACK_NAME}_api-server.entrypoints=simcore_api - traefik.http.routers.${SWARM_STACK_NAME}_api-server.priority=1 - - traefik.http.routers.${SWARM_STACK_NAME}_api-server.middlewares=${SWARM_STACK_NAME}_gzip@docker,ratelimit-${SWARM_STACK_NAME}_api-server,inflightreq-${SWARM_STACK_NAME}_api-server + - traefik.http.routers.${SWARM_STACK_NAME}_api-server.middlewares=${SWARM_STACK_NAME}_gzip@swarm,ratelimit-${SWARM_STACK_NAME}_api-server,inflightreq-${SWARM_STACK_NAME}_api-server networks: - default @@ -521,7 +521,7 @@ services: - traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.service=${SWARM_STACK_NAME}_static_webserver - traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.entrypoints=http - traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.priority=2 - - traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.middlewares=${SWARM_STACK_NAME}_gzip@docker,${SWARM_STACK_NAME}_static_webserver_retry + - traefik.http.routers.${SWARM_STACK_NAME}_static_webserver.middlewares=${SWARM_STACK_NAME}_gzip@swarm,${SWARM_STACK_NAME}_static_webserver_retry # catchall for legacy services (this happens if a backend disappears and a frontend tries to reconnect, the right return value is a 503) - traefik.http.routers.${SWARM_STACK_NAME}_legacy_services_catchall.service=${SWARM_STACK_NAME}_legacy_services_catchall - traefik.http.routers.${SWARM_STACK_NAME}_legacy_services_catchall.priority=1 @@ -749,7 +749,7 @@ services: - traefik.http.routers.${SWARM_STACK_NAME}_webserver.rule=hostregexp(`{host:.+}`) && (Path(`/`, `/v0`,`/socket.io/`,`/static-frontend-data.json`, `/study/{study_uuid:\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b}`, `/view`, `/#/view`, `/#/error`) || PathPrefix(`/v0/`)) - traefik.http.routers.${SWARM_STACK_NAME}_webserver.entrypoints=http - traefik.http.routers.${SWARM_STACK_NAME}_webserver.priority=2 - - traefik.http.routers.${SWARM_STACK_NAME}_webserver.middlewares=${SWARM_STACK_NAME}_gzip@docker, ${SWARM_STACK_NAME_NO_HYPHEN}_sslheader@docker, ${SWARM_STACK_NAME}_webserver_retry + - traefik.http.routers.${SWARM_STACK_NAME}_webserver.middlewares=${SWARM_STACK_NAME}_gzip@swarm, ${SWARM_STACK_NAME_NO_HYPHEN}_sslheader@swarm, ${SWARM_STACK_NAME}_webserver_retry networks: &webserver_networks - default - interactive_services_subnet @@ -1160,7 +1160,7 @@ services: retries: 50 traefik: - image: "traefik:v2.9.8@sha256:553239e27c4614d0477651415205b9b119f7a98f698e6562ef383c9d8ff3b6e6" + image: "traefik:v3.1.2@sha256:ec1a82940b8e00eaeef33fb4113aa1d1573b2ebb6440e10c023743fe96f08475" init: true hostname: "{{.Node.Hostname}}-{{.Task.Slot}}" command: @@ -1169,7 +1169,7 @@ services: - "--ping=true" - "--entryPoints.ping.address=:9082" - "--ping.entryPoint=ping" - - "--log.level=WARNING" + - "--log.level=WARN" # WARN, not WARNING - "--accesslog=false" - "--metrics.prometheus=true" - "--metrics.prometheus.addEntryPointsLabels=true" @@ -1182,17 +1182,18 @@ services: - "--entryPoints.simcore_api.forwardedHeaders.insecure" - "--entryPoints.traefik_monitor.address=:8080" - "--entryPoints.traefik_monitor.forwardedHeaders.insecure" - - "--providers.docker.endpoint=unix:///var/run/docker.sock" - - "--providers.docker.network=${SWARM_STACK_NAME}_default" - - "--providers.docker.swarmMode=true" + - "--providers.swarm.endpoint=unix:///var/run/docker.sock" + - "--providers.swarm.network=${SWARM_STACK_NAME}_default" # https://github.com/traefik/traefik/issues/7886 - - "--providers.docker.swarmModeRefreshSeconds=1" - - "--providers.docker.exposedByDefault=false" - - "--providers.docker.constraints=Label(`io.simcore.zone`, `${TRAEFIK_SIMCORE_ZONE}`)" - - "--tracing=true" - - "--tracing.jaeger=true" - - "--tracing.jaeger.samplingServerURL=http://jaeger:5778/sampling" - - "--tracing.jaeger.localAgentHostPort=jaeger:6831" + - "--providers.swarm.refreshSeconds=1" + - "--providers.swarm.exposedByDefault=false" + - "--providers.swarm.constraints=Label(`io.simcore.zone`, `${TRAEFIK_SIMCORE_ZONE}`)" + - "--core.defaultRuleSyntax=v2" + - "--tracing" + - "--tracing.addinternals" + - "--tracing.otlp=true" + - "--tracing.otlp.http=true" + # - "--tracing.otlp.http.endpoint=0.0.0.0:4318/v1/traces" volumes: # So that Traefik can listen to the Docker events - /var/run/docker.sock:/var/run/docker.sock diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 77973cd552d..c89f8b0aeeb 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -307,6 +307,10 @@ qx.Class.define("osparc.data.Resources", { getWithWallet2: { method: "GET", url: statics.API + "/services/-/resource-usages?wallet_id={walletId}&offset={offset}&limit={limit}" + }, + getUsagePerService: { + method: "GET", + url: statics.API + "/services/-/aggregated-usages?wallet_id={walletId}&aggregated_by=services&time_period={timePeriod}" } } }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/Node.js b/services/static-webserver/client/source/class/osparc/data/model/Node.js index 44921ae99d8..7e152805fb6 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Node.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Node.js @@ -712,7 +712,7 @@ qx.Class.define("osparc.data.model.Node", { if (outputs) { let hasOutputs = false; Object.keys(this.getOutputs()).forEach(outputKey => { - if (Object.hasOwn(outputs, outputKey)) { + if (outputKey in outputs) { this.setOutputs({ ...this.getOutputs(), [outputKey]: { @@ -1094,7 +1094,11 @@ qx.Class.define("osparc.data.model.Node", { }; this.fireDataEvent("showInLogger", msgData); - this.getIframeHandler().startPolling(); + if (this.getIframeHandler()) { + this.getIframeHandler().startPolling(); + } else { + console.error(this.getLabel() + " iframe handler not ready"); + } } }, diff --git a/services/static-webserver/client/source/class/osparc/data/model/Workbench.js b/services/static-webserver/client/source/class/osparc/data/model/Workbench.js index 4571dfba125..1110d55b736 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Workbench.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Workbench.js @@ -66,6 +66,13 @@ qx.Class.define("osparc.data.model.Workbench", { init: null, nullable: false, event: "changeStudy" + }, + + deserialized: { + check: "Boolean", + init: false, + nullable: false, + event: "changeDeserialized" } }, @@ -662,6 +669,7 @@ qx.Class.define("osparc.data.model.Workbench", { this.__deserializeEdges(workbenchInitData); workbenchInitData = null; workbenchUIInitData = null; + this.setDeserialized(true); }); }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js index 72b9bb2e351..7302f93ee7e 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js @@ -364,23 +364,6 @@ qx.Class.define("osparc.desktop.MainPage", { }); }, - closeStudy: function(studyId) { - if (studyId === undefined) { - if (this.__studyEditor && this.__studyEditor.getStudy()) { - studyId = this.__studyEditor.getStudy().getUuid(); - } else { - return; - } - } - const params = { - url: { - "studyId": studyId - }, - data: osparc.utils.Utils.getClientSessionID() - }; - osparc.data.Resources.fetch("studies", "close", params); - }, - __getStudyEditor: function() { if (this.__studyEditor) { return this.__studyEditor; diff --git a/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js b/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js index a4ab28e7d7d..e4817393152 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/SlideshowView.js @@ -364,6 +364,7 @@ qx.Class.define("osparc.desktop.SlideshowView", { startSlides: function() { // If the study is not initialized this will fail if (!this.isPropertyInitialized("study")) { + console.error("study is not initialized"); return; } const study = this.getStudy(); diff --git a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js index b00f3d11e07..02f7604615d 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -797,7 +797,8 @@ qx.Class.define("osparc.desktop.StudyEditor", { }, data: osparc.utils.Utils.getClientSessionID() }; - osparc.data.Resources.fetch("studies", "close", params); + osparc.data.Resources.fetch("studies", "close", params) + .catch(err => console.error(err)); }, closeEditor: function() { diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsIndicatorButton.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsIndicatorButton.js index ec7049f7ea0..2a1c13bd73e 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsIndicatorButton.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsIndicatorButton.js @@ -33,9 +33,6 @@ qx.Class.define("osparc.desktop.credits.CreditsIndicatorButton", { height: 24 }); - this.__creditsContainer = new osparc.desktop.credits.CreditsNavBarContainer(); - this.__creditsContainer.exclude(); - this.addListener("tap", this.__buttonTapped, this); }, @@ -53,6 +50,11 @@ qx.Class.define("osparc.desktop.credits.CreditsIndicatorButton", { }, __showCreditsContainer: function() { + if (!this.__creditsContainer) { + this.__creditsContainer = new osparc.desktop.credits.CreditsSummary(); + this.__creditsContainer.exclude(); + } + const tapListener = event => { // In case a notification was tapped propagate the event so it can be handled by the NotificationUI if (osparc.utils.Utils.isMouseOnElement(this.__creditsContainer, event)) { diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsNavBarContainer.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsNavBarContainer.js deleted file mode 100644 index 9ec7a2a271b..00000000000 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsNavBarContainer.js +++ /dev/null @@ -1,84 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2024 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -qx.Class.define("osparc.desktop.credits.CreditsNavBarContainer", { - extend: qx.ui.core.Widget, - - construct: function() { - this.base(arguments); - - this._setLayout(new qx.ui.layout.Grow()); - - this.set({ - appearance: "floating-menu", - padding: 8, - maxWidth: this.self().WIDTH - }); - osparc.utils.Utils.setIdToWidget(this, "creditsNavBarContainer"); - - const layout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); - - const creditsIndicator = new osparc.desktop.credits.CreditsIndicator(); - const store = osparc.store.Store.getInstance(); - store.bind("contextWallet", creditsIndicator, "wallet"); - layout.add(creditsIndicator, { - flex: 1 - }); - - const buttonSize = 26; - const billingCenterButton = new qx.ui.form.Button().set({ - appearance: "form-button-outlined", - width: buttonSize, - height: buttonSize, - alignX: "center", - alignY: "middle", - center: true, - icon: "@FontAwesome5Solid/ellipsis-v/12" - }); - // make it circular - billingCenterButton.getContentElement().setStyles({ - "border-radius": `${buttonSize / 2}px` - }); - billingCenterButton.addListener("execute", () => { - osparc.desktop.credits.BillingCenterWindow.openWindow(); - this.exclude(); - }); - osparc.utils.Utils.setIdToWidget(billingCenterButton, "billingCenterButton"); - layout.add(billingCenterButton); - - this._add(layout); - - const root = qx.core.Init.getApplication().getRoot(); - root.add(this, { - top: 0, - right: 0 - }); - }, - - statics: { - WIDTH: 200 - }, - - members: { - setPosition: function(x, y) { - this.setLayoutProperties({ - left: x - this.self().WIDTH, - top: y - }); - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsPerService.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsPerService.js new file mode 100644 index 00000000000..546f2135217 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsPerService.js @@ -0,0 +1,91 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.CreditsPerService", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(5)); + + this.initDaysRange(); + }, + + properties: { + daysRange: { + check: [1, 7, 30], + nullable: false, + init: 1, + apply: "__populateList" + } + }, + + members: { + __populateList: function(nDays) { + this._removeAll(); + + const store = osparc.store.Store.getInstance(); + const contextWallet = store.getContextWallet(); + if (!contextWallet) { + return; + } + const walletId = contextWallet.getWalletId(); + const loadingImage = new qx.ui.basic.Image("@FontAwesome5Solid/circle-notch/26").set({ + alignX: "center", + padding: 6 + }); + loadingImage.getContentElement().addClass("rotate"); + this._add(loadingImage); + + const params = { + "url": { + walletId, + "timePeriod": nDays + } + }; + osparc.data.Resources.fetch("resourceUsage", "getUsagePerService", params) + .then(entries => { + this._removeAll(); + if (entries && entries.length) { + let totalCredits = 0; + entries.forEach(entry => totalCredits+= entry["osparc_credits"]); + let datas = []; + entries.forEach(entry => { + datas.push({ + service: entry["service_key"], + credits: -1*entry["osparc_credits"], + percentage: 100*entry["osparc_credits"]/totalCredits, + }); + }); + datas.sort((a, b) => b.percentage - a.percentage); + // top 5 services + datas = datas.slice(0, 5); + datas.forEach(data => { + const uiEntry = new osparc.desktop.credits.CreditsServiceListItem(data.service, data.credits, data.percentage); + this._add(uiEntry); + }); + } else { + const nothingFound = new qx.ui.basic.Label(this.tr("No usage found")).set({ + font: "text-14" + }); + this._add(nothingFound); + } + }); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js new file mode 100644 index 00000000000..e8489cffb4e --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js @@ -0,0 +1,124 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.CreditsServiceListItem", { + extend: osparc.ui.list.ListItem, + + construct: function(serviceKey, credits, percentage) { + this.base(arguments); + + const layout = this._getLayout(); + layout.setSpacingX(12); + layout.setSpacingY(4); + layout.setColumnFlex(this.self().GRID.ICON.column, 0); + layout.setColumnFlex(this.self().GRID.NAME.column, 1); + layout.setColumnFlex(this.self().GRID.CREDITS.column, 0); + + const icon = this.getChildControl("icon"); + const name = this.getChildControl("title"); + const serviceMetadata = osparc.service.Utils.getLatest(serviceKey); + if (serviceMetadata) { + icon.setSource(serviceMetadata["thumbnail"] ? serviceMetadata["thumbnail"] : osparc.dashboard.CardBase.PRODUCT_ICON); + name.setValue(serviceMetadata["name"]); + } else { + icon.setSource(osparc.dashboard.CardBase.PRODUCT_ICON); + const serviceName = serviceKey.split("/").pop(); + name.setValue(serviceName); + } + this.getChildControl("percentage").set({ + maximum: 100, + value: percentage + }); + this.getChildControl("credits").setValue(credits + " used"); + }, + + statics: { + GRID: { + ICON: { + column: 0, + row: 0, + rowSpan: 2 + }, + NAME: { + column: 1, + row: 0 + }, + PERCENTAGE: { + column: 1, + row: 1 + }, + CREDITS: { + column: 2, + row: 0, + rowSpan: 2 + } + } + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "icon": { + control = new osparc.ui.basic.Thumbnail(null, 32, 32).set({ + minHeight: 32, + minWidth: 32 + }); + control.getChildControl("image").getContentElement().setStyles({ + "border-radius": "4px" + }); + this._add(control, this.self().GRID.ICON); + break; + } + case "title": + control = new qx.ui.basic.Label().set({ + font: "text-12", + alignY: "middle", + maxWidth: 200, + allowGrowX: true, + rich: true, + }); + this._add(control, this.self().GRID.NAME); + break; + case "percentage": + control = new qx.ui.indicator.ProgressBar().set({ + backgroundColor: "transparent", + maxHeight: 8, + margin: 0, + padding: 0 + }); + control.getChildControl("progress").set({ + backgroundColor: "strong-main" + }); + control.getContentElement().setStyles({ + "border-radius": "2px" + }); + this._add(control, this.self().GRID.PERCENTAGE); + break; + case "credits": + control = new qx.ui.basic.Label().set({ + font: "text-14", + alignY: "middle" + }); + this._add(control, this.self().GRID.CREDITS); + break; + } + + return control || this.base(arguments, id); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsSummary.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsSummary.js new file mode 100644 index 00000000000..1d077749802 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsSummary.js @@ -0,0 +1,162 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2024 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.CreditsSummary", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(10)); + + this.set({ + appearance: "floating-menu", + padding: 8 + }); + osparc.utils.Utils.setIdToWidget(this, "creditsSummary"); + + this.__buildLayout(); + + const root = qx.core.Init.getApplication().getRoot(); + root.add(this, { + top: 0, + right: 0 + }); + }, + + statics: { + BILLING_CENTER_BUTTON_SIZE: 26, + WIDTH: 300, + TIME_RANGES: [{ + key: 1, + label: "Today" + }, { + key: 7, + label: "Last week" + }, { + key: 30, + label: "Last month" + }] + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "top-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + this._add(control); + break; + case "top-left-spacer": { + const buttonSize = this.self().BILLING_CENTER_BUTTON_SIZE; + control = new qx.ui.core.Spacer(buttonSize, buttonSize); + const topLayout = this.getChildControl("top-layout"); + topLayout.add(control); + break; + } + case "credits-indicator": { + control = new osparc.desktop.credits.CreditsIndicator(); + control.getChildControl("credits-bar").exclude(); + const store = osparc.store.Store.getInstance(); + store.bind("contextWallet", control, "wallet"); + const topLayout = this.getChildControl("top-layout"); + topLayout.add(control, { + flex: 1 + }); + break; + } + case "billing-center-button": { + const buttonSize = this.self().BILLING_CENTER_BUTTON_SIZE; + control = new qx.ui.form.Button().set({ + appearance: "form-button-outlined", + width: buttonSize, + height: buttonSize, + alignX: "center", + alignY: "middle", + center: true, + icon: "@FontAwesome5Solid/ellipsis-v/12" + }); + // make it circular + control.getContentElement().setStyles({ + "border-radius": `${buttonSize / 2}px` + }); + control.addListener("execute", () => { + osparc.desktop.credits.BillingCenterWindow.openWindow(); + this.exclude(); + }); + osparc.utils.Utils.setIdToWidget(control, "billingCenterButton"); + const topLayout = this.getChildControl("top-layout"); + topLayout.add(control); + break; + } + case "time-range-sb": { + control = new qx.ui.form.SelectBox().set({ + allowGrowX: false, + alignX: "center", + backgroundColor: "transparent" + }); + this.self().TIME_RANGES.forEach(tr => { + const trItem = new qx.ui.form.ListItem(tr.label, null, tr.key); + control.add(trItem); + }); + // default one week + const found = control.getSelectables().find(trItem => trItem.getModel() === 7); + if (found) { + control.setSelection([found]); + } + this._add(control); + break; + } + case "services-consumption": + control = new osparc.desktop.credits.CreditsPerService(); + this._add(control); + break; + } + return control || this.base(arguments, id); + }, + + setPosition: function(x, y) { + this.setLayoutProperties({ + left: x - this.self().WIDTH, + top: y + }); + }, + + __buildLayout: function() { + this.getChildControl("top-left-spacer"); + this.getChildControl("credits-indicator"); + this.getChildControl("billing-center-button"); + this.__buildConsumptionSummary(); + + this.set({ + maxWidth: this.self().WIDTH + }) + }, + + __buildConsumptionSummary: function() { + const timeRangeSB = this.getChildControl("time-range-sb"); + const servicesConsumption = this.getChildControl("services-consumption"); + + timeRangeSB.addListener("changeSelection", e => { + const selection = e.getData(); + if (selection.length) { + servicesConsumption.setDaysRange(selection[0].getModel()); + } + }); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js index 1b618203f1f..2cdfb2c1f74 100644 --- a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js +++ b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js @@ -114,8 +114,13 @@ qx.Class.define("osparc.node.LifeCycleView", { }); updateButton.addListener("execute", () => { updateButton.setFetching(true); - const latestCompatibleMetadata = osparc.service.Utils.getLatestCompatible(node.getKey(), node.getVersion()); - node.setVersion(latestCompatibleMetadata["version"]); + const latestCompatible = osparc.service.Utils.getLatestCompatible(node.getKey(), node.getVersion()); + if (node.getKey() !== latestCompatible["key"]) { + node.setKey(latestCompatible["key"]); + } + if (node.getVersion() !== latestCompatible["version"]) { + node.setVersion(latestCompatible["version"]); + } node.fireEvent("updateStudyDocument"); setTimeout(() => { updateButton.setFetching(false); diff --git a/services/static-webserver/client/source/class/osparc/service/Utils.js b/services/static-webserver/client/source/class/osparc/service/Utils.js index 3175b48c717..38ce2b31905 100644 --- a/services/static-webserver/client/source/class/osparc/service/Utils.js +++ b/services/static-webserver/client/source/class/osparc/service/Utils.js @@ -229,8 +229,8 @@ qx.Class.define("osparc.service.Utils", { RETIRED_AUTOUPDATABLE_INSTRUCTIONS: qx.locale.Manager.tr("Please Update the Service"), isUpdatable: function(metadata) { - const latestCompatibleMetadata = this.getLatestCompatible(metadata["key"], metadata["version"]); - return latestCompatibleMetadata && (metadata["key"] !== latestCompatibleMetadata["key"] || metadata["version"] !== latestCompatibleMetadata["version"]); + const latestCompatible = this.getLatestCompatible(metadata["key"], metadata["version"]); + return latestCompatible && (metadata["key"] !== latestCompatible["key"] || metadata["version"] !== latestCompatible["version"]); }, isDeprecated: function(metadata) { diff --git a/services/static-webserver/client/source/class/osparc/study/Utils.js b/services/static-webserver/client/source/class/osparc/study/Utils.js index b0558d336b0..91dd4475551 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -65,9 +65,7 @@ qx.Class.define("osparc.study.Utils", { isWorkbenchUpdatable: function(workbench) { const services = new Set(this.extractServices(workbench)); - const isUpdatable = Array.from(services).some(srv => { - return osparc.service.Utils.getLatestCompatible(srv.key, srv.version) !== null; - }); + const isUpdatable = Array.from(services).some(srv => osparc.service.Utils.isUpdatable(srv)); return isUpdatable; }, diff --git a/services/static-webserver/client/source/class/osparc/utils/Utils.js b/services/static-webserver/client/source/class/osparc/utils/Utils.js index 93ccf8858a5..3b4b78061e0 100644 --- a/services/static-webserver/client/source/class/osparc/utils/Utils.js +++ b/services/static-webserver/client/source/class/osparc/utils/Utils.js @@ -679,6 +679,14 @@ qx.Class.define("osparc.utils.Utils", { return L > 0.35 ? "#FFF" : "#000"; }, + namedColorToHex: function(namedColor) { + if (qx.util.ExtendedColor.isExtendedColor(namedColor)) { + const rgb = qx.util.ExtendedColor.toRgb(namedColor); + return qx.util.ColorUtil.rgbToHexString(rgb); + } + return "#888888"; + }, + bytesToSize: function(bytes, decimals = 2, isDecimalCollapsed = true) { if (!+bytes) { return "0 Bytes"; diff --git a/services/static-webserver/client/source/class/osparc/viewer/NodeViewer.js b/services/static-webserver/client/source/class/osparc/viewer/NodeViewer.js index d455d3b9b8f..face09ef10f 100644 --- a/services/static-webserver/client/source/class/osparc/viewer/NodeViewer.js +++ b/services/static-webserver/client/source/class/osparc/viewer/NodeViewer.js @@ -25,41 +25,51 @@ qx.Class.define("osparc.viewer.NodeViewer", { this._setLayout(new qx.ui.layout.VBox()); - let studyData = null; - this.self().openStudy(studyId) - .then(resp => { - studyData = resp; - if (studyData["workbench"] && nodeId in studyData["workbench"]) { - const nodeData = studyData["workbench"][nodeId]; - return osparc.service.Store.getService(nodeData.key, nodeData.version) - } - throw new Error("Node data not found in Study"); - }) - .then(metadata => { + const params = { + url: { + studyId + }, + data: osparc.utils.Utils.getClientSessionID() + }; + osparc.data.Resources.fetch("studies", "open", params) + .then(studyData => { // create study const study = new osparc.data.model.Study(studyData); this.setStudy(study); - // create node - const node = new osparc.data.model.Node(study, metadata, nodeId); - this.setNode(node); - - node.addListener("retrieveInputs", e => { - const data = e.getData(); - const portKey = data["portKey"]; - node.retrieveInputs(portKey); - }, this); - - node.initIframeHandler(); - - const iframeHandler = node.getIframeHandler(); - if (iframeHandler) { - iframeHandler.startPolling(); - iframeHandler.addListener("iframeChanged", () => this.__iFrameChanged(), this); - iframeHandler.getIFrame().addListener("load", () => this.__iFrameChanged(), this); - this.__iFrameChanged(); + const startPolling = () => { + const node = study.getWorkbench().getNode(nodeId); + this.setNode(node); + + node.addListener("retrieveInputs", e => { + const data = e.getData(); + const portKey = data["portKey"]; + node.retrieveInputs(portKey); + }, this); + + node.initIframeHandler(); + + const iframeHandler = node.getIframeHandler(); + if (iframeHandler) { + iframeHandler.startPolling(); + iframeHandler.addListener("iframeChanged", () => this.__iFrameChanged(), this); + iframeHandler.getIFrame().addListener("load", () => this.__iFrameChanged(), this); + this.__iFrameChanged(); + + this.__attachSocketEventHandlers(); + } else { + console.error(node.getLabel() + " iframe handler not ready"); + } + } - this.__attachSocketEventHandlers(); + if (study.getWorkbench().isDeserialized()) { + startPolling(); + } else { + study.getWorkbench().addListener("changeDeserialized", e => { + if (e.getData()) { + startPolling(); + } + }); } }) .catch(err => console.error(err)); @@ -79,18 +89,6 @@ qx.Class.define("osparc.viewer.NodeViewer", { } }, - statics: { - openStudy: function(studyId) { - const params = { - url: { - "studyId": studyId - }, - data: osparc.utils.Utils.getClientSessionID() - }; - return osparc.data.Resources.fetch("studies", "open", params); - } - }, - members: { __iFrameChanged: function() { this._removeAll(); diff --git a/services/static-webserver/client/source/class/osparc/workbench/Annotation.js b/services/static-webserver/client/source/class/osparc/workbench/Annotation.js index fb2bb848d2f..904b71125eb 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/Annotation.js +++ b/services/static-webserver/client/source/class/osparc/workbench/Annotation.js @@ -33,10 +33,14 @@ qx.Class.define("osparc.workbench.Annotation", { if (id === undefined) { id = osparc.utils.Utils.uuidV4(); } + let color = "color" in data ? data.color : this.getColor(); + if (color && color[0] !== "#") { + color = osparc.utils.Utils.namedColorToHex(color); + } this.set({ id, type: data.type, - color: "color" in data ? data.color : this.getColor(), + color, attributes: data.attributes }); }, diff --git a/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js b/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js index a772507832b..1684b780fc6 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js +++ b/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js @@ -230,8 +230,10 @@ qx.Class.define("osparc.workbench.ServiceCatalog", { const groupedServicesList = []; for (const key in filteredServicesObj) { const serviceMetadata = osparc.service.Utils.getLatest(key); - const service = new osparc.data.model.Service(serviceMetadata); - groupedServicesList.push(service); + if (serviceMetadata) { + const service = new osparc.data.model.Service(serviceMetadata); + groupedServicesList.push(service); + } } this.__serviceList.setModel(new qx.data.Array(groupedServicesList)); diff --git a/services/static-webserver/client/source/resource/osparc/tours/tis_tours.json b/services/static-webserver/client/source/resource/osparc/tours/tis_tours.json index 592dcbeea9d..c44afd38aad 100644 --- a/services/static-webserver/client/source/resource/osparc/tours/tis_tours.json +++ b/services/static-webserver/client/source/resource/osparc/tours/tis_tours.json @@ -104,7 +104,7 @@ "selector": "osparc-test-id=creditsIndicatorButton", "event": "tap" }, - "anchorEl": "osparc-test-id=creditsNavBarContainer", + "anchorEl": "osparc-test-id=creditsSummary", "title": "Credits Indicator", "text": "By clicking on the Credits indicator, you will you see the number of available Credits. Click the three dots to access the Billing Center.", "placement": "left" diff --git a/services/static-webserver/client/tools/qooxdoo-kit/builder/Dockerfile b/services/static-webserver/client/tools/qooxdoo-kit/builder/Dockerfile index 7303f79ec5d..5890069e77b 100644 --- a/services/static-webserver/client/tools/qooxdoo-kit/builder/Dockerfile +++ b/services/static-webserver/client/tools/qooxdoo-kit/builder/Dockerfile @@ -59,7 +59,7 @@ RUN \ python3 ./scripts/post-compile.py -FROM joseluisq/static-web-server:1.16.0-alpine as server-base +FROM joseluisq/static-web-server:2.32.1-alpine as server-base LABEL org.opencontainers.image.authors="GitHK, odeimaiz" diff --git a/services/web/server/src/simcore_service_webserver/catalog/_api_units.py b/services/web/server/src/simcore_service_webserver/catalog/_api_units.py index 76a84842d49..a0367b60030 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_api_units.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_api_units.py @@ -45,7 +45,6 @@ def replace_service_input_outputs( ): """Thin wrapper to replace i/o ports in returned service model""" # This is a fast solution until proper models are available for the web API - for input_key in service["inputs"]: new_input: ServiceInputGet = ( ServiceInputGetFactory.from_catalog_service_api_model( diff --git a/services/web/server/src/simcore_service_webserver/catalog/_models.py b/services/web/server/src/simcore_service_webserver/catalog/_models.py index 59f0b5827c3..969b9a9f52d 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_models.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_models.py @@ -1,3 +1,5 @@ +import logging +import os from dataclasses import dataclass from typing import Any, Final @@ -11,6 +13,8 @@ from models_library.services import BaseServiceIOModel from pint import PintError, UnitRegistry +_logger = logging.getLogger(__name__) + def get_unit_name(port: BaseServiceIOModel) -> str | None: unit: str | None = port.unit @@ -56,10 +60,10 @@ def get_html_formatted_unit( # - the least recently used items will be discarded first to make space when necessary. # -_CACHE_MAXSIZE: Final = ( - 100 # number of items i.e. ServiceInputGet/ServiceOutputGet insteances -) -_CACHE_TTL: Final = 60 # secs +_CACHE_MAXSIZE: Final = int( + os.getenv("CACHETOOLS_CACHE_MAXSIZE", "100") +) # number of items i.e. ServiceInputGet/ServiceOutputGet instances +_CACHE_TTL: Final = int(os.getenv("CACHETOOLS_CACHE_TTL_SECS", "60")) # secs def _hash_inputs( @@ -71,9 +75,19 @@ def _hash_inputs( return f"{service['key']}/{service['version']}/{input_key}" +def _cachetools_cached(*args, **kwargs): + def decorator(func): + if os.getenv("CACHETOOLS_DISABLE", "0") == "0": + return cachetools.cached(*args, **kwargs)(func) + _logger.warning("cachetools disabled") + return func + + return decorator + + class ServiceInputGetFactory: @staticmethod - @cachetools.cached( + @_cachetools_cached( cachetools.TTLCache(ttl=_CACHE_TTL, maxsize=_CACHE_MAXSIZE), key=_hash_inputs ) def from_catalog_service_api_model( @@ -107,7 +121,7 @@ def _hash_outputs( class ServiceOutputGetFactory: @staticmethod - @cachetools.cached( + @_cachetools_cached( cachetools.TTLCache(ttl=_CACHE_TTL, maxsize=_CACHE_MAXSIZE), key=_hash_outputs ) def from_catalog_service_api_model( diff --git a/services/web/server/tests/unit/with_dbs/docker-compose-devel.yml b/services/web/server/tests/unit/with_dbs/docker-compose-devel.yml index 3cfff68f24f..198e2d6e462 100644 --- a/services/web/server/tests/unit/with_dbs/docker-compose-devel.yml +++ b/services/web/server/tests/unit/with_dbs/docker-compose-devel.yml @@ -62,7 +62,16 @@ services: image: rediscommander/redis-commander:latest restart: always environment: - - REDIS_HOSTS=resources:redis:6379:0,locks:redis:6379:1,validation_codes:redis:6379:2,scheduled_maintenance:redis:6379:3,user_notifications:redis:6379:4,announcements:redis:6379:5 + - >- + REDIS_HOSTS= + resources:redis:6379:0:${TEST_REDIS_PASSWORD}, + locks:redis:6379:1:${TEST_REDIS_PASSWORD}, + validation_codes:redis:6379:2:${TEST_REDIS_PASSWORD}, + scheduled_maintenance:redis:6379:3:${TEST_REDIS_PASSWORD}, + user_notifications:redis:6379:4:${TEST_REDIS_PASSWORD}, + announcements:redis:6379:5:${TEST_REDIS_PASSWORD}, + distributed_identifiers:redis:6379:6:${TEST_REDIS_PASSWORD}, + deferred_tasks:redis:6379:7:${TEST_REDIS_PASSWORD} ports: - "18081:8081" diff --git a/tests/e2e-playwright/Makefile b/tests/e2e-playwright/Makefile index f0ccd4322c0..af4a8d48538 100644 --- a/tests/e2e-playwright/Makefile +++ b/tests/e2e-playwright/Makefile @@ -131,27 +131,33 @@ $(SLEEPERS_INPUT_FILE) $(JUPYTER_LAB_INPUT_FILE) $(CLASSIC_TIP_INPUT_FILE) $(S4L read -p "Enter the size of the large file (human readable form e.g. 3Gib): " LARGE_FILE_SIZE; \ echo "--service-key=jupyter-math --large-file-size=$$LARGE_FILE_SIZE" >> $@; \ elif [ "$@" = "$(S4L_INPUT_FILE)" ]; then \ - echo "--service-key=sim4life-8-0-0-dy" >> $@; \ + echo "--plus_button_test_id=startS4LButton" >> $@; \ read -p "Do you want to check the videostreaming ? (requires to run with chrome/msedge) [y/n]: " VIDEOSTREAM; \ if [ "$$VIDEOSTREAM" = "y" ]; then \ echo "--check-videostreaming" >> $@; \ fi; \ + read -p "Do you want to use the plus button (NOTE: if yes then pass the osparc-test-ID of the plus button in the service key) ? [y/n]: " PLUS_BUTTON; \ + if [ "$$PLUS_BUTTON" = "y" ]; then \ + echo "--use-plus-button" >> $@; \ + fi; \ + read -p "Enter the service key: " SERVICE_KEY; \ + echo "--service-key=$$SERVICE_KEY" >> $@; \ elif [ "$@" = "$(SLEEPERS_INPUT_FILE)" ]; then \ read -p "Enter the number of sleepers: " NUM_SLEEPERS; \ echo "--num-sleepers=$$NUM_SLEEPERS" >> $@; \ fi # Run the tests -test-sleepers-anywhere: _check_venv_active $(SLEEPERS_INPUT_FILE) +test-sleepers-anywhere: _check_venv_active $(SLEEPERS_INPUT_FILE) ## run sleepers test and cache settings @$(call run_test, $(SLEEPERS_INPUT_FILE), tests/sleepers/test_sleepers.py) -test-s4l-anywhere: _check_venv_active $(S4L_INPUT_FILE) +test-s4l-anywhere: _check_venv_active $(S4L_INPUT_FILE) ## run s4l test and cache settings @$(call run_test_on_chrome, $(S4L_INPUT_FILE), tests/sim4life/test_sim4life.py) -test-jupyterlab-anywhere: _check_venv_active $(JUPYTER_LAB_INPUT_FILE) +test-jupyterlab-anywhere: _check_venv_active $(JUPYTER_LAB_INPUT_FILE) ## run jupyterlab test and cache settings @$(call run_test, $(JUPYTER_LAB_INPUT_FILE), tests/jupyterlabs/test_jupyterlab.py) -test-tip-anywhere: _check_venv_active $(CLASSIC_TIP_INPUT_FILE) +test-tip-anywhere: _check_venv_active $(CLASSIC_TIP_INPUT_FILE) ## run classic tip test and cache settings $(call run_test, $(CLASSIC_TIP_INPUT_FILE), tests/tip/test_ti_plan.py) # Define the common test running function diff --git a/tests/e2e-playwright/tests/conftest.py b/tests/e2e-playwright/tests/conftest.py index 26992facc38..cdae590ad8e 100644 --- a/tests/e2e-playwright/tests/conftest.py +++ b/tests/e2e-playwright/tests/conftest.py @@ -453,6 +453,19 @@ def _( _INNER_CONTEXT_TIMEOUT_MS = 0.8 * _OUTER_CONTEXT_TIMEOUT_MS +@pytest.fixture +def start_study_from_plus_button( + page: Page, +) -> Callable[[str], None]: + def _(plus_button_test_id: str) -> None: + with log_context( + logging.INFO, f"Find plus button {plus_button_test_id=} in study browser" + ): + page.get_by_test_id(plus_button_test_id).click() + + return _ + + @pytest.fixture def find_and_start_service_in_dashboard( page: Page, @@ -474,6 +487,21 @@ def _( return _ +@pytest.fixture +def create_project_from_new_button( + start_study_from_plus_button: Callable[[str], None], + create_new_project_and_delete: Callable[ + [tuple[RunningState], bool], dict[str, Any] + ], +) -> Callable[[str], dict[str, Any]]: + def _(plus_button_test_id: str) -> dict[str, Any]: + start_study_from_plus_button(plus_button_test_id) + expected_states = (RunningState.UNKNOWN,) + return create_new_project_and_delete(expected_states, False) + + return _ + + @pytest.fixture def create_project_from_service_dashboard( find_and_start_service_in_dashboard: Callable[[ServiceType, str, str | None], None], diff --git a/tests/e2e-playwright/tests/sim4life/conftest.py b/tests/e2e-playwright/tests/sim4life/conftest.py index d41448d97fc..0f927d22f13 100644 --- a/tests/e2e-playwright/tests/sim4life/conftest.py +++ b/tests/e2e-playwright/tests/sim4life/conftest.py @@ -19,9 +19,21 @@ def pytest_addoption(parser: pytest.Parser) -> None: default=False, help="check if video streaming is functional", ) + group.addoption( + "--use-plus-button", + action="store_true", + default=False, + help="The service key option will be used as the plus button ID instead of service key", + ) @pytest.fixture(scope="session") def check_videostreaming(request: pytest.FixtureRequest) -> bool: check_video = request.config.getoption("--check-videostreaming") return TypeAdapter(bool).validate_python(check_video) + + +@pytest.fixture(scope="session") +def use_plus_button(request: pytest.FixtureRequest) -> bool: + use_plus_button = request.config.getoption("--use-plus-button") + return TypeAdapter(bool).validate_python(use_plus_button) diff --git a/tests/e2e-playwright/tests/sim4life/test_sim4life.py b/tests/e2e-playwright/tests/sim4life/test_sim4life.py index b130fbae9e6..3fca6f36b54 100644 --- a/tests/e2e-playwright/tests/sim4life/test_sim4life.py +++ b/tests/e2e-playwright/tests/sim4life/test_sim4life.py @@ -113,14 +113,19 @@ def test_sim4life( create_project_from_service_dashboard: Callable[ [ServiceType, str, str | None], dict[str, Any] ], + create_project_from_new_button: Callable[[str], dict[str, Any]], log_in_and_out: WebSocket, service_key: str, + use_plus_button: bool, autoscaled: bool, check_videostreaming: bool, ): - project_data = create_project_from_service_dashboard( - ServiceType.DYNAMIC, service_key, None - ) + if use_plus_button: + project_data = create_project_from_new_button(service_key) + else: + project_data = create_project_from_service_dashboard( + ServiceType.DYNAMIC, service_key, None + ) assert "workbench" in project_data, "Expected workbench to be in project data!" assert isinstance( project_data["workbench"], dict @@ -128,7 +133,7 @@ def test_sim4life( node_ids: list[str] = list(project_data["workbench"]) assert len(node_ids) == 1, "Expected 1 node in the workbench!" - with log_context(logging.INFO, "launch S4l") as ctx: + with log_context(logging.INFO, "launch S4L") as ctx: predicate = _S4LWaitForWebsocket(logger=ctx.logger) with page.expect_websocket( predicate,