diff --git a/ci/github/system-testing/e2e-playwright.bash b/ci/github/system-testing/e2e-playwright.bash index 88d45bd86a4..b0a6e498f66 100755 --- a/ci/github/system-testing/e2e-playwright.bash +++ b/ci/github/system-testing/e2e-playwright.bash @@ -24,6 +24,7 @@ test() { source .venv/bin/activate pushd tests/e2e-playwright make test-sleepers + make test-platform popd } diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py index f063b6efd61..22ea24baad0 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright.py @@ -2,19 +2,25 @@ import json import logging import re +import typing from collections import defaultdict -from collections.abc import Generator, Iterator +from collections.abc import Generator from dataclasses import dataclass, field from datetime import UTC, datetime, timedelta from enum import Enum, unique from typing import Any, Final import httpx +from playwright._impl._sync_base import EventContextManager from playwright.sync_api import FrameLocator, Page, Request from playwright.sync_api import TimeoutError as PlaywrightTimeoutError from playwright.sync_api import WebSocket from pydantic import AnyUrl -from pytest_simcore.helpers.logging_tools import log_context + +from .logging_tools import log_context + +_logger = logging.getLogger(__name__) + SECOND: Final[int] = 1000 MINUTE: Final[int] = 60 * SECOND @@ -106,6 +112,94 @@ class SocketIOEvent: SOCKETIO_MESSAGE_PREFIX: Final[str] = "42" +@dataclass +class RestartableWebSocket: + page: Page + ws: WebSocket + _registered_events: list[tuple[str, typing.Callable | None]] = field( + default_factory=list + ) + _number_of_restarts: int = 0 + + def __post_init__(self): + self._configure_websocket_events() + + def _configure_websocket_events(self): + try: + with log_context( + logging.DEBUG, + msg="handle websocket message (set to --log-cli-level=DEBUG level if you wanna see all of them)", + ) as ctx: + + def on_framesent(payload: str | bytes) -> None: + ctx.logger.debug("⬇️ Frame sent: %s", payload) + + def on_framereceived(payload: str | bytes) -> None: + ctx.logger.debug("⬆️ Frame received: %s", payload) + + def on_close(_: WebSocket) -> None: + ctx.logger.warning( + "⚠️ WebSocket closed. Attempting to reconnect..." + ) + self._attempt_reconnect(ctx.logger) + + def on_socketerror(error_msg: str) -> None: + ctx.logger.error("❌ WebSocket error: %s", error_msg) + + # Attach core event listeners + self.ws.on("framesent", on_framesent) + self.ws.on("framereceived", on_framereceived) + self.ws.on("close", on_close) + self.ws.on("socketerror", on_socketerror) + + finally: + # Detach core event listeners + self.ws.remove_listener("framesent", on_framesent) + self.ws.remove_listener("framereceived", on_framereceived) + self.ws.remove_listener("close", on_close) + self.ws.remove_listener("socketerror", on_socketerror) + + def _attempt_reconnect(self, logger: logging.Logger) -> None: + """ + Attempt to reconnect the WebSocket and restore event listeners. + """ + try: + with self.page.expect_websocket() as ws_info: + assert not ws_info.value.is_closed() + + self.ws = ws_info.value + self._number_of_restarts += 1 + logger.info( + "🔄 Reconnected to WebSocket successfully. Number of reconnections: %s", + self._number_of_restarts, + ) + self._configure_websocket_events() + # Re-register all custom event listeners + for event, predicate in self._registered_events: + self.ws.expect_event(event, predicate) + + except Exception as e: # pylint: disable=broad-except + logger.error("🚨 Failed to reconnect WebSocket: %s", e) + + def expect_event( + self, + event: str, + predicate: typing.Callable | None = None, + *, + timeout: float | None = None, + ) -> EventContextManager: + """ + Register an event listener with support for reconnection. + """ + output = self.ws.expect_event(event, predicate, timeout=timeout) + self._registered_events.append((event, predicate)) + return output + + @classmethod + def create(cls, page: Page, ws: WebSocket): + return cls(page, ws) + + def decode_socketio_42_message(message: str) -> SocketIOEvent: data = json.loads(message.removeprefix(SOCKETIO_MESSAGE_PREFIX)) return SocketIOEvent(name=data[0], obj=data[1]) @@ -245,9 +339,14 @@ def __call__(self, message: str) -> bool: url = f"https://{self.node_id}.services.{self.get_partial_product_url()}" response = httpx.get(url, timeout=10) self.logger.info( - "Querying the service endpoint from the E2E test. Url: %s Response: %s", + "Querying the service endpoint from the E2E test. Url: %s Response: %s TIP: %s", url, response, + ( + "Response 401 is OK. It means that service is ready." + if response.status_code == 401 + else "We are emulating the frontend; a 500 response is acceptable if the service is not yet ready." + ), ) if response.status_code <= 401: # NOTE: If the response status is less than 400, it means that the backend is ready (There are some services that respond with a 3XX) @@ -278,7 +377,7 @@ def get_partial_product_url(self): def wait_for_pipeline_state( current_state: RunningState, *, - websocket: WebSocket, + websocket: RestartableWebSocket, if_in_states: tuple[RunningState, ...], expected_states: tuple[RunningState, ...], timeout_ms: int, @@ -301,39 +400,6 @@ def wait_for_pipeline_state( return current_state -@contextlib.contextmanager -def web_socket_default_log_handler(web_socket: WebSocket) -> Iterator[None]: - - try: - with log_context( - logging.DEBUG, - msg="handle websocket message (set to --log-cli-level=DEBUG level if you wanna see all of them)", - ) as ctx: - - def on_framesent(payload: str | bytes) -> None: - ctx.logger.debug("⬇️ Frame sent: %s", payload) - - def on_framereceived(payload: str | bytes) -> None: - ctx.logger.debug("⬆️ Frame received: %s", payload) - - def on_close(payload: WebSocket) -> None: - ctx.logger.warning("⚠️ Websocket closed: %s", payload) - - def on_socketerror(error_msg: str) -> None: - ctx.logger.error("❌ Websocket error: %s", error_msg) - - web_socket.on("framesent", on_framesent) - web_socket.on("framereceived", on_framereceived) - web_socket.on("close", on_close) - web_socket.on("socketerror", on_socketerror) - yield - finally: - web_socket.remove_listener("framesent", on_framesent) - web_socket.remove_listener("framereceived", on_framereceived) - web_socket.remove_listener("close", on_close) - web_socket.remove_listener("socketerror", on_socketerror) - - def _node_started_predicate(request: Request) -> bool: return bool( re.search(NODE_START_REQUEST_PATTERN, request.url) @@ -358,12 +424,14 @@ def expected_service_running( *, page: Page, node_id: str, - websocket: WebSocket, + websocket: RestartableWebSocket, timeout: int, press_start_button: bool, product_url: AnyUrl, ) -> Generator[ServiceRunning, None, None]: - with log_context(logging.INFO, msg="Waiting for node to run") as ctx: + with log_context( + logging.INFO, msg=f"Waiting for node to run. Timeout: {timeout}" + ) as ctx: waiter = SocketIONodeProgressCompleteWaiter( node_id=node_id, logger=ctx.logger, product_url=product_url ) @@ -395,7 +463,7 @@ def wait_for_service_running( *, page: Page, node_id: str, - websocket: WebSocket, + websocket: RestartableWebSocket, timeout: int, press_start_button: bool, product_url: AnyUrl, @@ -403,7 +471,9 @@ def wait_for_service_running( """NOTE: if the service was already started this will not work as some of the required websocket events will not be emitted again In which case this will need further adjutment""" - with log_context(logging.INFO, msg="Waiting for node to run") as ctx: + with log_context( + logging.INFO, msg=f"Waiting for node to run. Timeout: {timeout}" + ) as ctx: waiter = SocketIONodeProgressCompleteWaiter( node_id=node_id, logger=ctx.logger, product_url=product_url ) diff --git a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py index c59718f4aff..8c5b74d032b 100644 --- a/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py +++ b/packages/pytest-simcore/src/pytest_simcore/helpers/playwright_sim4life.py @@ -13,6 +13,7 @@ MINUTE, SECOND, SOCKETIO_MESSAGE_PREFIX, + RestartableWebSocket, SocketIOEvent, decode_socketio_42_message, wait_for_service_running, @@ -100,7 +101,7 @@ class WaitForS4LDict(TypedDict): def wait_for_launched_s4l( page: Page, node_id, - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, *, autoscaled: bool, copy_workspace: bool, diff --git a/services/director/src/simcore_service_director/core/settings.py b/services/director/src/simcore_service_director/core/settings.py index 2773f02fb51..852e5369fe1 100644 --- a/services/director/src/simcore_service_director/core/settings.py +++ b/services/director/src/simcore_service_director/core/settings.py @@ -4,7 +4,7 @@ from fastapi import FastAPI from models_library.basic_types import LogLevel, PortInt, VersionTag -from pydantic import AliasChoices, Field, NonNegativeInt, field_validator +from pydantic import Field, NonNegativeInt, PositiveInt, field_validator from servicelib.logging_utils_filtering import LoggerName, MessageSubstring from settings_library.application import BaseApplicationSettings from settings_library.docker_registry import RegistrySettings @@ -112,6 +112,9 @@ class ApplicationSettings(BaseApplicationSettings, MixinLoggingSettings): ), ) + DIRECTOR_REGISTRY_CLIENT_MAX_CONCURRENT_CALLS: PositiveInt = 20 + DIRECTOR_REGISTRY_CLIENT_MAX_NUMBER_OF_RETRIEVED_OBJECTS: PositiveInt = 30 + @field_validator("DIRECTOR_GENERIC_RESOURCE_PLACEMENT_CONSTRAINTS_SUBSTITUTIONS") @classmethod def _validate_substitutions(cls, v): diff --git a/services/director/src/simcore_service_director/registry_proxy.py b/services/director/src/simcore_service_director/registry_proxy.py index f45f3b96348..3099627ddc6 100644 --- a/services/director/src/simcore_service_director/registry_proxy.py +++ b/services/director/src/simcore_service_director/registry_proxy.py @@ -5,7 +5,7 @@ from collections.abc import Mapping from http import HTTPStatus from pprint import pformat -from typing import Any, Final, cast +from typing import Any, cast from aiocache import Cache, SimpleMemoryCache # type: ignore[import-untyped] from aiohttp import BasicAuth, ClientSession, client_exceptions @@ -33,9 +33,6 @@ DEPENDENCIES_LABEL_KEY: str = "simcore.service.dependencies" -NUMBER_OF_RETRIEVED_REPOS: int = 50 -NUMBER_OF_RETRIEVED_TAGS: int = 50 -_MAX_CONCURRENT_CALLS: Final[int] = 50 VERSION_REG = re.compile( r"^(0|[1-9]\d*)(\.(0|[1-9]\d*)){2}(-(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*)(\.(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*))*)?(\+[-\da-zA-Z]+(\.[-\da-zA-Z-]+)*)?$" ) @@ -277,7 +274,7 @@ async def on_shutdown() -> None: async def _list_repositories(app: FastAPI) -> list[str]: logger.debug("listing repositories") # if there are more repos, the Link will be available in the response headers until none available - path = f"/v2/_catalog?n={NUMBER_OF_RETRIEVED_REPOS}" + path = f"/v2/_catalog?n={get_application_settings(app).DIRECTOR_REGISTRY_CLIENT_MAX_NUMBER_OF_RETRIEVED_OBJECTS}" repos_list: list = [] while True: result, headers = await registry_request(app, path) @@ -294,7 +291,7 @@ async def list_image_tags(app: FastAPI, image_key: str) -> list[str]: logger.debug("listing image tags in %s", image_key) image_tags: list = [] # get list of image tags - path = f"/v2/{image_key}/tags/list?n={NUMBER_OF_RETRIEVED_TAGS}" + path = f"/v2/{image_key}/tags/list?n={get_application_settings(app).DIRECTOR_REGISTRY_CLIENT_MAX_NUMBER_OF_RETRIEVED_OBJECTS}" while True: tags, headers = await registry_request(app, path) if tags["tags"]: @@ -385,7 +382,9 @@ async def get_repo_details(app: FastAPI, image_key: str) -> list[dict[str, Any]] *[get_image_details(app, image_key, tag) for tag in image_tags], reraise=False, log=logger, - limit=_MAX_CONCURRENT_CALLS, + limit=get_application_settings( + app + ).DIRECTOR_REGISTRY_CLIENT_MAX_CONCURRENT_CALLS, ) return [result for result in results if not isinstance(result, BaseException)] @@ -407,7 +406,9 @@ async def list_services(app: FastAPI, service_type: ServiceType) -> list[dict]: *[get_repo_details(app, repo) for repo in repos], reraise=False, log=logger, - limit=_MAX_CONCURRENT_CALLS, + limit=get_application_settings( + app + ).DIRECTOR_REGISTRY_CLIENT_MAX_CONCURRENT_CALLS, ) return [ diff --git a/services/opentelemetry-collector-config.yaml b/services/opentelemetry-collector-config.yaml index 14640ab61da..7386666956e 100644 --- a/services/opentelemetry-collector-config.yaml +++ b/services/opentelemetry-collector-config.yaml @@ -13,10 +13,16 @@ service: traces: receivers: [otlp] exporters: [otlphttp] - processors: [batch,probabilistic_sampler] + processors: [batch,probabilistic_sampler,filter/drop_healthcheck] processors: batch: timeout: 5s send_batch_size: ${TRACING_OPENTELEMETRY_COLLECTOR_BATCH_SIZE} probabilistic_sampler: sampling_percentage: ${TRACING_OPENTELEMETRY_COLLECTOR_SAMPLING_PERCENTAGE} + filter/drop_healthcheck: + error_mode: ignore + traces: + span: + - attributes["http.route"] == "healthcheck_readiness_probe" + - attributes["db.statement"] == "PING" and attributes["db.system"] == "redis" diff --git a/services/static-webserver/client/source/class/osparc/announcement/AnnouncementUIFactory.js b/services/static-webserver/client/source/class/osparc/announcement/AnnouncementUIFactory.js index 8f126387642..6533fc0444a 100644 --- a/services/static-webserver/client/source/class/osparc/announcement/AnnouncementUIFactory.js +++ b/services/static-webserver/client/source/class/osparc/announcement/AnnouncementUIFactory.js @@ -19,14 +19,8 @@ qx.Class.define("osparc.announcement.AnnouncementUIFactory", { extend: qx.core.Object, type: "singleton", - properties: { - announcement: { - check: "osparc.announcement.Announcement", - init: null, - nullable: false, - event: "changeAnnouncement", - apply: "__applyAnnouncement" - } + events: { + "changeAnnouncements": "qx.event.type.Event", }, statics: { @@ -63,14 +57,9 @@ qx.Class.define("osparc.announcement.AnnouncementUIFactory", { } return loginAnnouncement; - } - }, - - members: { - __ribbonAnnouncement: null, + }, - __isValid: function(widgetType) { - const announcement = this.getAnnouncement(); + isValid: function(announcement, widgetType) { if (announcement) { const now = new Date(); const validPeriod = now > announcement.getStart() && now < announcement.getEnd(); @@ -80,59 +69,105 @@ qx.Class.define("osparc.announcement.AnnouncementUIFactory", { } return false; }, + }, - __applyAnnouncement: function() { - if (this.__ribbonAnnouncement) { - osparc.notification.RibbonNotifications.getInstance().removeNotification(this.__ribbonAnnouncement); - this.__ribbonAnnouncement = null; - } - if (this.__hasRibbonAnnouncement()) { - this.__addRibbonAnnouncement(); - } + members: { + __announcements: null, + __ribbonAnnouncements: null, + + setAnnouncementsData: function(announcementsData) { + this.__announcements = []; + announcementsData.forEach(announcementData => { + const announcement = new osparc.announcement.Announcement(announcementData); + this.__announcements.push(announcement); + }); + this.fireEvent("changeAnnouncements"); + + this.__addToRibbon(); }, - hasLoginAnnouncement: function() { - return this.__isValid("login"); + __addToRibbon: function() { + if (this.__ribbonAnnouncements && this.__ribbonAnnouncements.length) { + this.__ribbonAnnouncements.forEach(ribbonAnnouncement => { + osparc.notification.RibbonNotifications.getInstance().removeNotification(ribbonAnnouncement); + }); + } + this.__ribbonAnnouncements = []; + this.__announcements.forEach(announcement => { + if (this.self().isValid(announcement, "ribbon")) { + const ribbonAnnouncement = this.__addRibbonAnnouncement(announcement); + if (ribbonAnnouncement) { + this.__ribbonAnnouncements.push(ribbonAnnouncement); + } + } + }); }, - __hasRibbonAnnouncement: function() { - return this.__isValid("ribbon"); + hasLoginAnnouncement: function() { + return this.__announcements && this.__announcements.some(announcement => this.self().isValid(announcement, "login")); }, hasUserMenuAnnouncement: function() { - return this.__isValid("user-menu") && this.getAnnouncement().getLink(); + return this.__announcements && this.__announcements.some(announcement => this.self().isValid(announcement, "ribbon") && announcement.getLink()); }, - createLoginAnnouncement: function() { - const announcement = this.getAnnouncement(); - const loginAnnouncement = this.self().createLoginAnnouncement(announcement.getTitle(), announcement.getDescription()); - return loginAnnouncement; + createLoginAnnouncements: function() { + const loginAnnouncements = []; + this.__announcements.forEach(announcement => { + if (this.self().isValid(announcement, "login")) { + const loginAnnouncement = this.self().createLoginAnnouncement(announcement.getTitle(), announcement.getDescription()) + loginAnnouncement.setWidth(osparc.auth.core.BaseAuthPage.FORM_WIDTH-5); // show 1-2 pixel of the nearby announcement + loginAnnouncements.push(loginAnnouncement); + } + }); + if (loginAnnouncements.length === 1) { + return loginAnnouncements[0]; + } + const slideBar = new osparc.widget.SlideBar().set({ + allowGrowX: true, + }); + slideBar.getChildControl("button-backward").set({ + backgroundColor: "transparent" + }); + slideBar.getChildControl("button-forward").set({ + backgroundColor: "transparent" + }); + loginAnnouncements.forEach(loginAnnouncement => slideBar.add(loginAnnouncement)); + return slideBar; }, - __addRibbonAnnouncement: function() { - const announcement = this.getAnnouncement(); - + __addRibbonAnnouncement: function(announcement) { if (osparc.utils.Utils.localCache.isDontShowAnnouncement(announcement.getId())) { - return; + return null; } - let text = announcement.getTitle(); + let text = ""; + if (announcement.getTitle()) { + text += announcement.getTitle(); + } + if (announcement.getTitle() && announcement.getDescription()) { + text += ": "; + } if (announcement.getDescription()) { - text += ": " + announcement.getDescription(); + text += announcement.getDescription(); } - - const ribbonAnnouncement = this.__ribbonAnnouncement = new osparc.notification.RibbonNotification(text, "announcement", true); + const ribbonAnnouncement = new osparc.notification.RibbonNotification(text, "announcement", true); ribbonAnnouncement.announcementId = announcement.getId(); osparc.notification.RibbonNotifications.getInstance().addNotification(ribbonAnnouncement); + return ribbonAnnouncement; }, - createUserMenuAnnouncement: function() { - const announcement = this.getAnnouncement(); - - const link = announcement.getLink(); - const userMenuAnnouncement = new qx.ui.menu.Button(announcement.getTitle() + "..."); - userMenuAnnouncement.addListener("execute", () => window.open(link)); - return userMenuAnnouncement; + createUserMenuAnnouncements: function() { + const userMenuAnnouncements = []; + this.__announcements.forEach(announcement => { + if (this.self().isValid(announcement, "user-menu")) { + const link = announcement.getLink(); + const userMenuAnnouncement = new qx.ui.menu.Button(announcement.getTitle() + "..."); + userMenuAnnouncement.addListener("execute", () => window.open(link)); + userMenuAnnouncements.push(userMenuAnnouncement); + } + }); + return userMenuAnnouncements; } } }); diff --git a/services/static-webserver/client/source/class/osparc/announcement/Tracker.js b/services/static-webserver/client/source/class/osparc/announcement/Tracker.js index da6a8f17e38..368fbd6dd00 100644 --- a/services/static-webserver/client/source/class/osparc/announcement/Tracker.js +++ b/services/static-webserver/client/source/class/osparc/announcement/Tracker.js @@ -25,21 +25,13 @@ qx.Class.define("osparc.announcement.Tracker", { members: { __checkInterval: null, - __announcements: null, checkAnnouncements: async function() { - return new Promise(resolve => { - osparc.data.Resources.get("announcements") - .then(announcements => { - if (announcements && announcements.length) { - this.__setAnnouncements(announcements); - } else { - this.__setAnnouncements(null); - } - resolve(); - }) - .catch(err => console.error(err)); - }); + osparc.data.Resources.get("announcements") + .then(announcements => { + osparc.announcement.AnnouncementUIFactory.getInstance().setAnnouncementsData((announcements && announcements.length) ? announcements : []); + }) + .catch(err => console.error(err)); }, startTracker: function() { @@ -52,15 +44,5 @@ qx.Class.define("osparc.announcement.Tracker", { clearInterval(this.__checkInterval); } }, - - __setAnnouncements: function(announcementsData) { - this.__announcements = {}; - if (announcementsData) { - announcementsData.forEach(announcementData => { - const announcement = new osparc.announcement.Announcement(announcementData); - osparc.announcement.AnnouncementUIFactory.getInstance().setAnnouncement(announcement); - }); - } - } } }); diff --git a/services/static-webserver/client/source/class/osparc/auth/ui/LoginView.js b/services/static-webserver/client/source/class/osparc/auth/ui/LoginView.js index d52f03e4784..56972d7eb6f 100644 --- a/services/static-webserver/client/source/class/osparc/auth/ui/LoginView.js +++ b/services/static-webserver/client/source/class/osparc/auth/ui/LoginView.js @@ -37,18 +37,22 @@ qx.Class.define("osparc.auth.ui.LoginView", { members: { __loginBtn: null, + __loginAnnouncements: null, // overrides base _buildPage: function() { const announcementUIFactory = osparc.announcement.AnnouncementUIFactory.getInstance(); + const addAnnouncements = () => { + if (this.__loginAnnouncements) { + this.remove(this.__loginAnnouncements); + } + this.__loginAnnouncements = announcementUIFactory.createLoginAnnouncements(); + this.addAt(this.__loginAnnouncements, 0); + }; if (announcementUIFactory.hasLoginAnnouncement()) { - this.addAt(announcementUIFactory.createLoginAnnouncement(), 0); + addAnnouncements(); } else { - announcementUIFactory.addListenerOnce("changeAnnouncement", e => { - if (announcementUIFactory.hasLoginAnnouncement()) { - this.addAt(announcementUIFactory.createLoginAnnouncement(), 0); - } - }); + announcementUIFactory.addListenerOnce("changeAnnouncements", () => addAnnouncements()); } // form diff --git a/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonItem.js index 0971a7d4990..2932a774c92 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonItem.js @@ -128,6 +128,7 @@ qx.Class.define("osparc.dashboard.FolderButtonItem", { control.getContentElement().setStyles({ "border-radius": `${osparc.dashboard.ListButtonItem.MENU_BTN_DIMENSIONS / 2}px` }); + osparc.utils.Utils.setIdToWidget(control, "folderItemMenuButton"); this._add(control, osparc.dashboard.FolderButtonBase.POS.MENU); break; } @@ -146,6 +147,8 @@ qx.Class.define("osparc.dashboard.FolderButtonItem", { folder.bind("name", this, "title"); folder.bind("lastModified", this, "lastModified"); + osparc.utils.Utils.setIdToWidget(this, "folderItem_" + folder.getFolderId()); + this.__addMenuButton(); }, @@ -211,11 +214,13 @@ qx.Class.define("osparc.dashboard.FolderButtonItem", { const moveToButton = new qx.ui.menu.Button(this.tr("Move to..."), "@FontAwesome5Solid/folder/12"); moveToButton.addListener("execute", () => this.fireDataEvent("moveFolderToRequested", this.getFolderId()), this); + osparc.utils.Utils.setIdToWidget(moveToButton, "moveFolderMenuItem"); menu.add(moveToButton); menu.addSeparator(); const deleteButton = new qx.ui.menu.Button(this.tr("Delete"), "@FontAwesome5Solid/trash/12"); + osparc.utils.Utils.setIdToWidget(deleteButton, "deleteFolderMenuItem"); deleteButton.addListener("execute", () => this.__deleteFolderRequested(), this); menu.add(deleteButton); } @@ -264,6 +269,7 @@ qx.Class.define("osparc.dashboard.FolderButtonItem", { confirmText: this.tr("Delete"), confirmAction: "delete" }); + osparc.utils.Utils.setIdToWidget(confirmationWin.getConfirmButton(), "confirmDeleteFolderButton"); confirmationWin.center(); confirmationWin.open(); confirmationWin.addListener("close", () => { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonNew.js b/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonNew.js index 627ff5ccab9..6fe4c7d9bba 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonNew.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/FolderButtonNew.js @@ -35,6 +35,8 @@ qx.Class.define("osparc.dashboard.FolderButtonNew", { this.setPriority(osparc.dashboard.CardBase.CARD_PRIORITY.NEW); this.__buildLayout(); + + osparc.utils.Utils.setIdToWidget(this, "newFolderButton"); }, events: { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js index 7f4f0362cab..3ff37cd8412 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js @@ -140,30 +140,89 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { const studyAlias = osparc.product.Utils.getStudyAlias({firstUpperCase: true}); this._showLoadingPage(this.tr("Creating ") + (templateData.name || studyAlias)); - const studyOptions = new osparc.study.StudyOptions(); - // they will be patched once the study is created - studyOptions.setPatchStudy(false); - studyOptions.setStudyData(templateData); - const win = osparc.study.StudyOptions.popUpInWindow(studyOptions); - win.moveItUp(); - const cancelStudyOptions = () => { - this._hideLoadingPage(); - win.close(); - } - win.addListener("cancel", () => cancelStudyOptions()); - studyOptions.addListener("cancel", () => cancelStudyOptions()); - studyOptions.addListener("startStudy", () => { - const newName = studyOptions.getChildControl("title-field").getValue(); - const walletSelection = studyOptions.getChildControl("wallet-selector").getSelection(); - const nodesPricingUnits = studyOptions.getChildControl("study-pricing-units").getNodePricingUnits(); - win.close(); - this._showLoadingPage(this.tr("Creating ") + (newName || studyAlias)); + if (osparc.desktop.credits.Utils.areWalletsEnabled()) { + const studyOptions = new osparc.study.StudyOptions(); + // they will be patched once the study is created + studyOptions.setPatchStudy(false); + studyOptions.setStudyData(templateData); + const win = osparc.study.StudyOptions.popUpInWindow(studyOptions); + win.moveItUp(); + const cancelStudyOptions = () => { + this._hideLoadingPage(); + win.close(); + } + win.addListener("cancel", () => cancelStudyOptions()); + studyOptions.addListener("cancel", () => cancelStudyOptions()); + studyOptions.addListener("startStudy", () => { + const newName = studyOptions.getChildControl("title-field").getValue(); + const walletSelection = studyOptions.getChildControl("wallet-selector").getSelection(); + const nodesPricingUnits = studyOptions.getChildControl("study-pricing-units").getNodePricingUnits(); + win.close(); + + this._showLoadingPage(this.tr("Creating ") + (newName || studyAlias)); + osparc.study.Utils.createStudyFromTemplate(templateData, this._loadingPage) + .then(newStudyData => { + const studyId = newStudyData["uuid"]; + const openCB = () => { + this._hideLoadingPage(); + }; + const cancelCB = () => { + this._hideLoadingPage(); + const params = { + url: { + studyId + } + }; + osparc.data.Resources.fetch("studies", "delete", params); + }; + + const promises = []; + // patch the name + if (newStudyData["name"] !== newName) { + promises.push(osparc.study.StudyOptions.updateName(newStudyData, newName)); + } + // patch the wallet + if (walletSelection.length && walletSelection[0]["walletId"]) { + const walletId = walletSelection[0]["walletId"]; + promises.push(osparc.study.StudyOptions.updateWallet(newStudyData["uuid"], walletId)); + } + // patch the pricing units + // the nodeIds are coming from the original template, they need to be mapped to the newStudy + const workbench = newStudyData["workbench"]; + const nodesIdsListed = []; + Object.keys(workbench).forEach(nodeId => { + const node = workbench[nodeId]; + if (osparc.study.StudyPricingUnits.includeInList(node)) { + nodesIdsListed.push(nodeId); + } + }); + nodesPricingUnits.forEach((nodePricingUnits, idx) => { + const selectedPricingUnitId = nodePricingUnits.getPricingUnits().getSelectedUnitId(); + if (selectedPricingUnitId) { + const nodeId = nodesIdsListed[idx]; + const pricingPlanId = nodePricingUnits.getPricingPlanId(); + promises.push(osparc.study.NodePricingUnits.patchPricingUnitSelection(studyId, nodeId, pricingPlanId, selectedPricingUnitId)); + } + }); + + Promise.all(promises) + .then(() => { + win.close(); + const showStudyOptions = false; + this._startStudyById(studyId, openCB, cancelCB, showStudyOptions); + }); + }) + .catch(err => { + this._hideLoadingPage(); + osparc.FlashMessenger.getInstance().logAs(err.message, "ERROR"); + console.error(err); + }); + }); + } else { osparc.study.Utils.createStudyFromTemplate(templateData, this._loadingPage) .then(newStudyData => { const studyId = newStudyData["uuid"]; - const openCB = () => { - this._hideLoadingPage(); - }; + const openCB = () => this._hideLoadingPage(); const cancelCB = () => { this._hideLoadingPage(); const params = { @@ -173,49 +232,15 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { }; osparc.data.Resources.fetch("studies", "delete", params); }; - - const promises = []; - // patch the name - if (newStudyData["name"] !== newName) { - promises.push(osparc.study.StudyOptions.updateName(newStudyData, newName)); - } - // patch the wallet - if (walletSelection.length && walletSelection[0]["walletId"]) { - const walletId = walletSelection[0]["walletId"]; - promises.push(osparc.study.StudyOptions.updateWallet(newStudyData["uuid"], walletId)); - } - // patch the pricing units - // the nodeIds are coming from the original template, they need to be mapped to the newStudy - const workbench = newStudyData["workbench"]; - const nodesIdsListed = []; - Object.keys(workbench).forEach(nodeId => { - const node = workbench[nodeId]; - if (osparc.study.StudyPricingUnits.includeInList(node)) { - nodesIdsListed.push(nodeId); - } - }); - nodesPricingUnits.forEach((nodePricingUnits, idx) => { - const selectedPricingUnitId = nodePricingUnits.getPricingUnits().getSelectedUnitId(); - if (selectedPricingUnitId) { - const nodeId = nodesIdsListed[idx]; - const pricingPlanId = nodePricingUnits.getPricingPlanId(); - promises.push(osparc.study.NodePricingUnits.patchPricingUnitSelection(studyId, nodeId, pricingPlanId, selectedPricingUnitId)); - } - }); - - Promise.all(promises) - .then(() => { - win.close(); - const showStudyOptions = false; - this._startStudyById(studyId, openCB, cancelCB, showStudyOptions); - }); + const isStudyCreation = true; + this._startStudyById(studyId, openCB, cancelCB, isStudyCreation); }) .catch(err => { this._hideLoadingPage(); osparc.FlashMessenger.getInstance().logAs(err.message, "ERROR"); console.error(err); }); - }); + } }, // LAYOUT // diff --git a/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonItem.js b/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonItem.js index 4d5253410bf..187be3598e2 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonItem.js @@ -127,6 +127,7 @@ qx.Class.define("osparc.dashboard.WorkspaceButtonItem", { control.getContentElement().setStyles({ "border-radius": `${this.self().MENU_BTN_DIMENSIONS / 2}px` }); + osparc.utils.Utils.setIdToWidget(control, "workspaceItemMenuButton"); layout = this.getChildControl("header"); layout.addAt(control, osparc.dashboard.WorkspaceButtonBase.HPOS.MENU); break; @@ -159,6 +160,8 @@ qx.Class.define("osparc.dashboard.WorkspaceButtonItem", { workspace.bind("accessRights", this, "accessRights"); workspace.bind("modifiedAt", this, "modifiedAt"); workspace.bind("myAccessRights", this, "myAccessRights"); + + osparc.utils.Utils.setIdToWidget(this, "workspaceItem_" + workspace.getWorkspaceId()); }, __applyTitle: function(value) { @@ -201,6 +204,7 @@ qx.Class.define("osparc.dashboard.WorkspaceButtonItem", { menu.addSeparator(); const deleteButton = new qx.ui.menu.Button(this.tr("Delete"), "@FontAwesome5Solid/trash/12"); + osparc.utils.Utils.setIdToWidget(deleteButton, "deleteWorkspaceMenuItem"); deleteButton.addListener("execute", () => this.__deleteWorkspaceRequested(), this); menu.add(deleteButton); @@ -254,6 +258,7 @@ qx.Class.define("osparc.dashboard.WorkspaceButtonItem", { confirmText: this.tr("Delete"), confirmAction: "delete" }); + osparc.utils.Utils.setIdToWidget(confirmationWin.getConfirmButton(), "confirmDeleteWorkspaceButton"); confirmationWin.center(); confirmationWin.open(); confirmationWin.addListener("close", () => { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonNew.js b/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonNew.js index ac87579355e..dd65702503b 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonNew.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/WorkspaceButtonNew.js @@ -43,6 +43,8 @@ qx.Class.define("osparc.dashboard.WorkspaceButtonNew", { opacity: 1 }); this.getChildControl("footer").exclude(); + + osparc.utils.Utils.setIdToWidget(this, "newWorkspaceButton"); }, events: { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTree.js b/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTree.js index 7f35c3ff320..e8914ca6166 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTree.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTree.js @@ -137,6 +137,12 @@ qx.Class.define("osparc.dashboard.WorkspacesAndFoldersTree", { return isOpen; }, }, item, id); + }, + configureItem: item => { + item.addListener("changeModel", e => { + const model = e.getData(); + osparc.utils.Utils.setIdToWidget(item, `workspacesAndFoldersTreeItem_${model.getWorkspaceId()}_${model.getFolderId()}`); + }) } }); diff --git a/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTreeItem.js b/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTreeItem.js index ccedd3b3b3d..13793622cb7 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTreeItem.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/WorkspacesAndFoldersTreeItem.js @@ -29,8 +29,6 @@ qx.Class.define("osparc.dashboard.WorkspacesAndFoldersTreeItem", { this.setNotHoveredStyle(); this.__attachEventHandlers(); - - osparc.utils.Utils.setIdToWidget(this, "workspacesAndFoldersTreeItem"); }, members: { diff --git a/services/static-webserver/client/source/class/osparc/data/Permissions.js b/services/static-webserver/client/source/class/osparc/data/Permissions.js index 44d00471160..5c53b4da76d 100644 --- a/services/static-webserver/client/source/class/osparc/data/Permissions.js +++ b/services/static-webserver/client/source/class/osparc/data/Permissions.js @@ -138,7 +138,6 @@ qx.Class.define("osparc.data.Permissions", { "study.nodestree.uuid.read", "study.filestree.uuid.read", "study.logger.debug.read", - "statics.read" ], "product_owner": [ "user.invitation.generate", 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 0a1be2d9ae8..7a37517f898 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 @@ -312,7 +312,19 @@ qx.Class.define("osparc.data.model.Node", { return outputs[outputKey]["value"]; } return null; - } + }, + + getLinkedNodeIds: function(nodeData) { + const linkedNodeIds = new Set([]); + if ("inputs" in nodeData) { + Object.values(nodeData["inputs"]).forEach(link => { + if (link && typeof link === "object" && "nodeUuid" in link) { + linkedNodeIds.add(link["nodeUuid"]); + } + }); + } + return Array.from(linkedNodeIds); + }, }, members: { diff --git a/services/static-webserver/client/source/class/osparc/data/model/Study.js b/services/static-webserver/client/source/class/osparc/data/model/Study.js index ab178aca669..dc96044e99f 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Study.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Study.js @@ -345,7 +345,18 @@ qx.Class.define("osparc.data.model.Study", { "STARTED", "RETRY" ].includes(state); - } + }, + + __isAnyLinkedNodeMissing: function(studyData) { + const existingNodeIds = Object.keys(studyData["workbench"]); + const linkedNodeIds = osparc.data.model.Workbench.getLinkedNodeIds(studyData["workbench"]); + const allExist = linkedNodeIds.every(linkedNodeId => existingNodeIds.includes(linkedNodeId)); + return !allExist; + }, + + isCorrupt: function(studyData) { + return this.__isAnyLinkedNodeMissing(studyData); + }, }, members: { 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 63cd8212fe2..822f5d59eff 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 @@ -78,7 +78,16 @@ qx.Class.define("osparc.data.model.Workbench", { statics: { CANT_ADD_NODE: qx.locale.Manager.tr("Nodes can't be added while the pipeline is running"), - CANT_DELETE_NODE: qx.locale.Manager.tr("Nodes can't be deleted while the pipeline is running") + CANT_DELETE_NODE: qx.locale.Manager.tr("Nodes can't be deleted while the pipeline is running"), + + getLinkedNodeIds: function(workbenchData) { + const linkedNodeIDs = new Set([]); + Object.values(workbenchData).forEach(nodeData => { + const linkedNodes = osparc.data.model.Node.getLinkedNodeIds(nodeData); + linkedNodes.forEach(linkedNodeID => linkedNodeIDs.add(linkedNodeID)) + }); + return Array.from(linkedNodeIDs); + }, }, members: { diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPageHandler.js b/services/static-webserver/client/source/class/osparc/desktop/MainPageHandler.js index 165d4cf5f08..0872a1b627b 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPageHandler.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPageHandler.js @@ -85,6 +85,8 @@ qx.Class.define("osparc.desktop.MainPageHandler", { }, loadStudy: function(studyData) { + const studyAlias = osparc.product.Utils.getStudyAlias({firstUpperCase: true}); + // check if it's locked let locked = false; let lockedBy = false; if ("state" in studyData && "locked" in studyData["state"]) { @@ -92,13 +94,20 @@ qx.Class.define("osparc.desktop.MainPageHandler", { lockedBy = studyData["state"]["locked"]["owner"]; } if (locked && lockedBy["user_id"] !== osparc.auth.Data.getInstance().getUserId()) { - const msg = `${qx.locale.Manager.tr("Study is already open by ")} ${ + const msg = `${studyAlias} ${qx.locale.Manager.tr("is already open by")} ${ "first_name" in lockedBy && lockedBy["first_name"] != null ? lockedBy["first_name"] : qx.locale.Manager.tr("another user.") }`; throw new Error(msg); } + + // check if it's corrupt + if (osparc.data.model.Study.isCorrupt(studyData)) { + const msg = `${qx.locale.Manager.tr("We encountered an issue with the")} ${studyAlias}
${qx.locale.Manager.tr("Please contact support.")}`; + throw new Error(msg); + } + this.setLoadingPageHeader(qx.locale.Manager.tr("Loading ") + studyData.name); this.showLoadingPage(); const inaccessibleServices = osparc.study.Utils.getInaccessibleServices(studyData["workbench"]) diff --git a/services/static-webserver/client/source/class/osparc/desktop/preferences/Preferences.js b/services/static-webserver/client/source/class/osparc/desktop/preferences/Preferences.js index 876019a22c6..d04b96ceeea 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/preferences/Preferences.js +++ b/services/static-webserver/client/source/class/osparc/desktop/preferences/Preferences.js @@ -32,9 +32,6 @@ qx.Class.define("osparc.desktop.preferences.Preferences", { if (osparc.product.Utils.showClusters()) { this.__addClustersPage(); } - if (osparc.data.Permissions.getInstance().canDo("statics.read")) { - this.__addTestersPage(); - } }, members: { @@ -85,12 +82,5 @@ qx.Class.define("osparc.desktop.preferences.Preferences", { .catch(err => console.error(err)); } }, - - __addTestersPage: function() { - const title = this.tr("Tester"); - const iconSrc = "@FontAwesome5Solid/user-md/24"; - const testerPage = new osparc.desktop.preferences.pages.TesterPage(); - this.addTab(title, iconSrc, testerPage); - } } }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TesterPage.js b/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TesterPage.js deleted file mode 100644 index c886995c744..00000000000 --- a/services/static-webserver/client/source/class/osparc/desktop/preferences/pages/TesterPage.js +++ /dev/null @@ -1,88 +0,0 @@ -/* ************************************************************************ - - 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) - -************************************************************************ */ - -/** - * Tester Misc in preferences dialog - */ - -qx.Class.define("osparc.desktop.preferences.pages.TesterPage", { - extend: qx.ui.core.Widget, - - construct: function() { - this.base(arguments); - - this._setLayout(new qx.ui.layout.VBox(15)); - - const container = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); - - const statics = this.__createStaticsLayout(); - container.add(statics); - - const localStorage = this.__createLocalStorageLayout(); - container.add(localStorage); - - const scroll = new qx.ui.container.Scroll(container); - this._add(scroll, { - flex: 1 - }); - }, - - members: { - __createStaticsLayout: function() { - // layout - const box = osparc.ui.window.TabbedView.createSectionBox(this.tr("Statics")); - - const label = osparc.ui.window.TabbedView.createHelpLabel(this.tr( - "This is a list of the 'statics' resources" - )); - box.add(label); - - const statics = osparc.store.Store.getInstance().get("statics"); - const form = new qx.ui.form.Form(); - for (let [key, value] of Object.entries(statics)) { - const textField = new qx.ui.form.TextField().set({ - value: typeof value === "object" ? JSON.stringify(value) : value.toString(), - readOnly: true - }); - form.add(textField, key, null, key); - } - box.add(new qx.ui.form.renderer.Single(form)); - - return box; - }, - - __createLocalStorageLayout: function() { - // layout - const box = osparc.ui.window.TabbedView.createSectionBox(this.tr("Local Storage")); - - const items = { - ...window.localStorage - }; - const form = new qx.ui.form.Form(); - for (let [key, value] of Object.entries(items)) { - const textField = new qx.ui.form.TextField().set({ - value: typeof value === "object" ? JSON.stringify(value) : value.toString(), - readOnly: true - }); - form.add(textField, key, null, key); - } - box.add(new qx.ui.form.renderer.Single(form)); - - return box; - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/editor/FolderEditor.js b/services/static-webserver/client/source/class/osparc/editor/FolderEditor.js index 42d2013293f..4e8238d4f7f 100644 --- a/services/static-webserver/client/source/class/osparc/editor/FolderEditor.js +++ b/services/static-webserver/client/source/class/osparc/editor/FolderEditor.js @@ -60,6 +60,7 @@ qx.Class.define("osparc.editor.FolderEditor", { placeholder: this.tr("Title"), height: 27 }); + osparc.utils.Utils.setIdToWidget(control, "folderEditorTitle"); this.bind("label", control, "value"); control.bind("value", this, "label"); this._add(control); @@ -78,6 +79,7 @@ qx.Class.define("osparc.editor.FolderEditor", { this.fireEvent("createFolder"); } }, this); + osparc.utils.Utils.setIdToWidget(control, "folderEditorCreate"); buttons.addAt(control, 1); break; } @@ -94,6 +96,7 @@ qx.Class.define("osparc.editor.FolderEditor", { this.fireEvent("updateFolder"); } }, this); + osparc.utils.Utils.setIdToWidget(control, "folderEditorSave"); buttons.addAt(control, 1); break; } diff --git a/services/static-webserver/client/source/class/osparc/editor/WorkspaceEditor.js b/services/static-webserver/client/source/class/osparc/editor/WorkspaceEditor.js index dab5a9807c3..4e151d4e433 100644 --- a/services/static-webserver/client/source/class/osparc/editor/WorkspaceEditor.js +++ b/services/static-webserver/client/source/class/osparc/editor/WorkspaceEditor.js @@ -125,6 +125,7 @@ qx.Class.define("osparc.editor.WorkspaceEditor", { placeholder: this.tr("Title"), height: 30, }); + osparc.utils.Utils.setIdToWidget(control, "workspaceEditorTitle"); this.bind("label", control, "value"); control.bind("value", this, "label"); this._addAt(control, this.self().POS.TITLE); @@ -170,6 +171,7 @@ qx.Class.define("osparc.editor.WorkspaceEditor", { control = new osparc.ui.form.FetchButton(this.tr("Save")).set({ appearance: "form-button" }); + osparc.utils.Utils.setIdToWidget(control, "workspaceEditorSave"); control.addListener("execute", () => this.__saveWorkspace(control), this); buttons.addAt(control, 1); break; diff --git a/services/static-webserver/client/source/class/osparc/file/FolderViewer.js b/services/static-webserver/client/source/class/osparc/file/FolderViewer.js index 23c1d5e57e8..26fb4433bf3 100644 --- a/services/static-webserver/client/source/class/osparc/file/FolderViewer.js +++ b/services/static-webserver/client/source/class/osparc/file/FolderViewer.js @@ -201,24 +201,6 @@ qx.Class.define("osparc.file.FolderViewer", { return control || this.base(arguments, id); }, - __getEmptyEntry: function() { - const items = []; - if (this.getMode() === "list") { - items.push([ - "", - this.tr("Empty folder"), - "", - "", - "" - ]); - } else if (this.getMode() === "icons") { - items.push(this.self().getItemButton().set({ - label: this.tr("Empty folder") - })); - } - return items; - }, - __convertEntries: function(content) { const items = []; if (this.getMode() === "list") { diff --git a/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js b/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js index b18f0a4d7b3..213bc8bf0bd 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js +++ b/services/static-webserver/client/source/class/osparc/navigation/UserMenu.js @@ -61,6 +61,11 @@ qx.Class.define("osparc.navigation.UserMenu", { control.addListener("execute", () => osparc.po.POCenterWindow.openWindow(), this); this.add(control); break; + case "tester-center": + control = new qx.ui.menu.Button(this.tr("Tester Center")); + control.addListener("execute", () => osparc.tester.TesterCenterWindow.openWindow(), this); + this.add(control); + break; case "billing-center": control = new qx.ui.menu.Button(this.tr("Billing Center")); osparc.utils.Utils.setIdToWidget(control, "userMenuBillingCenterBtn"); @@ -157,6 +162,9 @@ qx.Class.define("osparc.navigation.UserMenu", { if (osparc.data.Permissions.getInstance().isProductOwner()) { this.getChildControl("po-center"); } + if (osparc.data.Permissions.getInstance().isTester()) { + this.getChildControl("tester-center"); + } if (osparc.desktop.credits.Utils.areWalletsEnabled()) { this.getChildControl("billing-center"); } @@ -169,10 +177,7 @@ qx.Class.define("osparc.navigation.UserMenu", { this.getChildControl("theme-switcher"); this.addSeparator(); - const announcementUIFactory = osparc.announcement.AnnouncementUIFactory.getInstance(); - if (announcementUIFactory.hasUserMenuAnnouncement()) { - this.add(announcementUIFactory.createUserMenuAnnouncement()); - } + this.__addAnnouncements(); this.getChildControl("about"); if (osparc.product.Utils.showAboutProduct()) { this.getChildControl("about-product"); @@ -188,6 +193,13 @@ qx.Class.define("osparc.navigation.UserMenu", { osparc.utils.Utils.prettifyMenu(this); }, + __addAnnouncements: function() { + const announcementUIFactory = osparc.announcement.AnnouncementUIFactory.getInstance(); + if (announcementUIFactory.hasUserMenuAnnouncement()) { + announcementUIFactory.createUserMenuAnnouncements().forEach(announcement => this.add(announcement)); + } + }, + populateMenuCompact: function() { this.removeAll(); const authData = osparc.auth.Data.getInstance(); @@ -225,10 +237,7 @@ qx.Class.define("osparc.navigation.UserMenu", { this.getChildControl("theme-switcher"); this.addSeparator(); - const announcementUIFactory = osparc.announcement.AnnouncementUIFactory.getInstance(); - if (announcementUIFactory.hasUserMenuAnnouncement()) { - this.add(announcementUIFactory.createUserMenuAnnouncement()); - } + this.__addAnnouncements(); this.getChildControl("about"); if (!osparc.product.Utils.isProduct("osparc")) { this.getChildControl("about-product"); diff --git a/services/static-webserver/client/source/class/osparc/study/StudyOptions.js b/services/static-webserver/client/source/class/osparc/study/StudyOptions.js index 5b0fd30cadb..0ceef6fde4e 100644 --- a/services/static-webserver/client/source/class/osparc/study/StudyOptions.js +++ b/services/static-webserver/client/source/class/osparc/study/StudyOptions.js @@ -130,6 +130,8 @@ qx.Class.define("osparc.study.StudyOptions", { control = new qx.ui.form.TextField().set({ maxWidth: 220 }); + control.addListener("changeValue", () => this.__evaluateOpenButton()); + osparc.utils.Utils.setIdToWidget(control, "studyTitleField"); this.getChildControl("title-layout").add(control); break; case "wallet-selector-layout": @@ -140,6 +142,7 @@ qx.Class.define("osparc.study.StudyOptions", { control = osparc.desktop.credits.Utils.createWalletSelector("read").set({ allowGrowX: false }); + control.addListener("changeSelection", () => this.__evaluateOpenButton()); this.getChildControl("wallet-selector-layout").add(control); break; case "advanced-layout": @@ -226,7 +229,8 @@ qx.Class.define("osparc.study.StudyOptions", { minWidth: 150, maxWidth: 150, height: 35, - center: true + center: true, + enabled: false, }); osparc.utils.Utils.setIdToWidget(control, "openWithResources"); this.getChildControl("buttons-layout").addAt(control, 1); @@ -275,14 +279,20 @@ qx.Class.define("osparc.study.StudyOptions", { } }); } + }, - this.getChildControl("open-button").setEnabled(Boolean(wallet)); + __evaluateOpenButton: function() { + const hasTitle = Boolean(this.getChildControl("title-field").getValue()); + const walletSelected = Boolean(this.getChildControl("wallet-selector").getSelection().length); + this.getChildControl("open-button").setEnabled(hasTitle && walletSelected); }, __buildLayout: function() { this.__buildTopSummaryLayout(); this.__buildOptionsLayout(); this.__buildButtons(); + + this.__evaluateOpenButton(); }, __buildTopSummaryLayout: function() { diff --git a/services/static-webserver/client/source/class/osparc/tester/Statics.js b/services/static-webserver/client/source/class/osparc/tester/Statics.js new file mode 100644 index 00000000000..b6655075e9f --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/tester/Statics.js @@ -0,0 +1,75 @@ +/* ************************************************************************ + + 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.tester.Statics", { + extend: osparc.po.BaseView, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "statics-container": + control = osparc.ui.window.TabbedView.createSectionBox(this.tr("Statics")); + this._add(control, { + flex: 1 + }); + break; + case "statics-content": { + const statics = osparc.store.Store.getInstance().get("statics"); + const form = new qx.ui.form.Form(); + for (let [key, value] of Object.entries(statics)) { + const textField = new qx.ui.form.TextField().set({ + value: typeof value === "object" ? JSON.stringify(value) : value.toString(), + readOnly: true + }); + form.add(textField, key, null, key); + } + const renderer = new qx.ui.form.renderer.Single(form); + control = new qx.ui.container.Scroll(renderer); + this.getChildControl("statics-container").add(control); + break; + } + case "local-storage-container": + control = osparc.ui.window.TabbedView.createSectionBox(this.tr("Local Storage")); + this._add(control); + break; + case "local-storage-content": { + const items = { + ...window.localStorage + }; + const form = new qx.ui.form.Form(); + for (let [key, value] of Object.entries(items)) { + const textField = new qx.ui.form.TextField().set({ + value: typeof value === "object" ? JSON.stringify(value) : value.toString(), + readOnly: true + }); + form.add(textField, key, null, key); + } + control = new qx.ui.form.renderer.Single(form); + this.getChildControl("local-storage-container").add(control); + break; + } + } + return control || this.base(arguments, id); + }, + + _buildLayout: function() { + this.getChildControl("statics-content"); + this.getChildControl("local-storage-content"); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/tester/TesterCenter.js b/services/static-webserver/client/source/class/osparc/tester/TesterCenter.js new file mode 100644 index 00000000000..b456afebb32 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/tester/TesterCenter.js @@ -0,0 +1,48 @@ +/* ************************************************************************ + + 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.tester.TesterCenter", { + extend: osparc.ui.window.TabbedView, + + construct: function() { + this.base(arguments); + + const miniProfile = osparc.desktop.account.MyAccount.createMiniProfileView().set({ + paddingRight: 10 + }); + this.addWidgetOnTopOfTheTabs(miniProfile); + + this.__addSocketMessagesPage(); + this.__addStaticsPage(); + }, + + members: { + __addSocketMessagesPage: function() { + const title = this.tr("Socket Messages"); + const iconSrc = "@FontAwesome5Solid/exchange-alt/22"; + const maintenance = new osparc.tester.WebSocketMessages(); + this.addTab(title, iconSrc, maintenance); + }, + + __addStaticsPage: function() { + const title = this.tr("Statics"); + const iconSrc = "@FontAwesome5Solid/wrench/22"; + const maintenance = new osparc.tester.Statics(); + this.addTab(title, iconSrc, maintenance); + }, + } +}); diff --git a/services/static-webserver/client/source/class/osparc/tester/TesterCenterWindow.js b/services/static-webserver/client/source/class/osparc/tester/TesterCenterWindow.js new file mode 100644 index 00000000000..9b2a99c330d --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/tester/TesterCenterWindow.js @@ -0,0 +1,43 @@ +/* ************************************************************************ + + 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.tester.TesterCenterWindow", { + extend: osparc.ui.window.TabbedWindow, + + construct: function() { + this.base(arguments, "tester-center", this.tr("Tester Center")); + + const width = 800; + const maxHeight = 800; + this.set({ + width, + maxHeight, + }); + + const testerCenter = new osparc.tester.TesterCenter(); + this._setTabbedView(testerCenter); + }, + + statics: { + openWindow: function() { + const accountWindow = new osparc.tester.TesterCenterWindow(); + accountWindow.center(); + accountWindow.open(); + return accountWindow; + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/tester/WebSocketMessages.js b/services/static-webserver/client/source/class/osparc/tester/WebSocketMessages.js new file mode 100644 index 00000000000..0872301459f --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/tester/WebSocketMessages.js @@ -0,0 +1,138 @@ +/* ************************************************************************ + + 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.tester.WebSocketMessages", { + extend: osparc.po.BaseView, + construct: function() { + this.base(arguments); + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "filter-message": { + control = new qx.ui.form.TextField().set({ + liveUpdate : true, + placeholder: this.tr("Search in Message"), + }); + this._add(control); + break; + } + case "messages-table": { + const tableModel = new qx.ui.table.model.Filtered(); + tableModel.setColumns([ + this.tr("Date"), + this.tr("Channel"), + this.tr("Message"), + ]); + const custom = { + tableColumnModel: function(obj) { + return new qx.ui.table.columnmodel.Resize(obj); + } + }; + control = new qx.ui.table.Table(tableModel, custom).set({ + selectable: true, + statusBarVisible: false, + showCellFocusIndicator: false, + forceLineHeight: false + }); + control.getTableColumnModel().setDataCellRenderer( + 1, + new qx.ui.table.cellrenderer.String().set({ + defaultCellStyle: "user-select: text" + }) + ); + control.getTableColumnModel().setDataCellRenderer( + 0, + new qx.ui.table.cellrenderer.String().set({ + defaultCellStyle: "user-select: text" + }) + ); + control.getTableColumnModel().setDataCellRenderer( + 2, + new osparc.ui.table.cellrenderer.Html().set({ + defaultCellStyle: "user-select: text; text-wrap: wrap" + }) + ); + control.setColumnWidth(0, 80); + control.setColumnWidth(1, 150); + + control.setDataRowRenderer(new osparc.ui.table.rowrenderer.ExpandSelection(control)); + this._add(control, { + flex: 1 + }); + break; + } + case "json-viewer": + control = new osparc.ui.basic.JsonTreeWidget(); + this._add(control); + break; + } + return control || this.base(arguments, id); + }, + + _buildLayout: function() { + const filterMessage = this.getChildControl("filter-message"); + const table = this.getChildControl("messages-table"); + const jsonViewer = this.getChildControl("json-viewer"); + + const model = table.getTableModel(); + filterMessage.addListener("changeValue", e => { + const value = e.getData(); + model.resetHiddenRows(); + model.addNotRegex(value, "Message", true); + model.applyFilters(); + }); + table.addListener("cellTap", e => { + const selectedRow = e.getRow(); + const rowData = table.getTableModel().getRowData(selectedRow); + jsonViewer.setJson(JSON.parse(rowData[2])); + }, this); + + this.__populateTable(); + }, + + __populateTable: function() { + const socket = osparc.wrapper.WebSocket.getInstance(); + const messagesObj = socket.getCachedMessages(); + const messagesArray = []; + for (const channel in messagesObj) { + messagesObj[channel].forEach(msg => { + messagesArray.push({ + date: msg.date, + channel, + message: msg.message, + }); + }); + } + messagesArray.sort((a, b) => { + return new Date(b.date) - new Date(a.date); // newest first + }); + const datas = []; + messagesArray.forEach(entry => { + const data = [ + new Date(entry.date).toLocaleTimeString(), + entry.channel, + JSON.stringify(entry.message), + ]; + datas.push(data); + }); + this.getChildControl("messages-table").getTableModel().setData(datas); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js b/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js index 24453fe0bf8..60f5a55e5b1 100644 --- a/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js +++ b/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js @@ -36,11 +36,21 @@ qx.Class.define("osparc.ui.basic.JsonTreeWidget", { * @param data {Object} Json object to be displayed by JsonTreeViewer */ construct: function(data) { - const prettyJson = JSON.stringify(data, null, " ").replace(/\n/ig, "
"); - this.base(arguments, prettyJson); + this.base(arguments); this.set({ rich: true, selectable: true }); + + if (data) { + this.setJson(data); + } + }, + + members: { + setJson(data) { + const prettyJson = JSON.stringify(data, null, " ").replace(/\n/ig, "
"); + this.setValue(prettyJson); + } } }); diff --git a/services/static-webserver/client/source/class/osparc/widget/logger/LoggerView.js b/services/static-webserver/client/source/class/osparc/widget/logger/LoggerView.js index e77ff8c6840..7eb6bbb4081 100644 --- a/services/static-webserver/client/source/class/osparc/widget/logger/LoggerView.js +++ b/services/static-webserver/client/source/class/osparc/widget/logger/LoggerView.js @@ -21,7 +21,7 @@ * It consists of: * - a toolbar containing: * - clear button - * - filter as you type textfiled + * - filter as you type textfield * - some log type filtering buttons * - log messages table * diff --git a/services/static-webserver/client/source/class/osparc/wrapper/WebSocket.js b/services/static-webserver/client/source/class/osparc/wrapper/WebSocket.js index 3a300c433b9..081d790c01d 100644 --- a/services/static-webserver/client/source/class/osparc/wrapper/WebSocket.js +++ b/services/static-webserver/client/source/class/osparc/wrapper/WebSocket.js @@ -134,11 +134,13 @@ qx.Class.define("osparc.wrapper.WebSocket", { this.setNamespace(namespace); } this.__name = []; + this.__cache = {}; }, members: { // The name store an array of events __name: null, + __cache: null, /** * Trying to using socket.io to connect and plug every event from socket.io to qooxdoo one @@ -234,7 +236,7 @@ qx.Class.define("osparc.wrapper.WebSocket", { * Connect and event from socket.io like qooxdoo event * * @param {string} name The event name to watch - * @param {function} fn The function wich will catch event response + * @param {function} fn The function which will catch event response * @param {mixed} that A link to this * @returns {void} */ @@ -247,6 +249,21 @@ qx.Class.define("osparc.wrapper.WebSocket", { } else { socket.on(name, fn); } + + // add a duplicated slot listener to keep the messages cached + socket.on(name, message => { + if (!(name in this.__cache)) { + this.__cache[name] = []; + } + const info = { + date: new Date(), + message: message ? message : "", + } + this.__cache[name].unshift(info); + if (this.__cache[name].length > 20) { + this.__cache[name].length = 20; + } + }, this); } }, @@ -265,7 +282,11 @@ qx.Class.define("osparc.wrapper.WebSocket", { index = this.__name.indexOf(name); } } - } + }, + + getCachedMessages: function() { + return this.__cache; + }, }, /** @@ -281,6 +302,7 @@ qx.Class.define("osparc.wrapper.WebSocket", { } } this.__name = null; + this.__cache = null; this.removeAllBindings(); diff --git a/tests/e2e-playwright/Makefile b/tests/e2e-playwright/Makefile index fc4c0463de2..40fe914b9c0 100644 --- a/tests/e2e-playwright/Makefile +++ b/tests/e2e-playwright/Makefile @@ -64,7 +64,8 @@ install-dev install-prod install-ci: _check_venv_active ## install app in develo RETRY_DURATION_SECONDS := 30 RETRY_INTERVAL_SECONDS := 1 -install-ci-up-simcore: install-ci +.PHONY: install-ci-up-simcore +install-ci-up-simcore: install-ci ## run registry and simcore stack locally (push sleepers image and modifies DB) @$(MAKE_C) $(REPO_BASE_DIR) local-registry @$(_transfer-images-to-registry) @$(_up_simcore) @@ -106,6 +107,28 @@ test-sleepers-dev: _check_venv_active ## runs sleepers test on local deploy --autoregister \ $(CURDIR)/tests/sleepers/test_sleepers.py +.PHONY: test-platform +test-platform: _check_venv_active ## runs platform test on local deploy + @pytest \ + -sxvv \ + --color=yes \ + --product-url=http://$(get_my_ip):9081 \ + --autoregister \ + --tracing=on \ + $(CURDIR)/tests/platform_CI_tests/test_platform.py + +.PHONY: test-platform-dev +test-platform-dev: _check_venv_active ## runs platform test on local deploy (with PWDEBUG=1) + @PWDEBUG=1 pytest \ + -sxvv \ + --color=yes \ + --pdb \ + --product-url=http://$(get_my_ip):9081 \ + --headed \ + --autoregister \ + --tracing=on \ + $(CURDIR)/tests/platform_CI_tests/test_platform.py + # Define the files where user input will be saved SLEEPERS_INPUT_FILE := .e2e-playwright-sleepers-env.txt diff --git a/tests/e2e-playwright/tests/conftest.py b/tests/e2e-playwright/tests/conftest.py index 6fd15e8218c..ca2ec5a579e 100644 --- a/tests/e2e-playwright/tests/conftest.py +++ b/tests/e2e-playwright/tests/conftest.py @@ -11,7 +11,6 @@ import os import random import re -import time import urllib.parse from collections.abc import Callable, Iterator from contextlib import ExitStack @@ -20,7 +19,7 @@ import arrow import pytest from faker import Faker -from playwright.sync_api import APIRequestContext, BrowserContext, Page, WebSocket +from playwright.sync_api import APIRequestContext, BrowserContext, Page from playwright.sync_api._generated import Playwright from pydantic import AnyUrl, TypeAdapter from pytest_simcore.helpers.faker_factories import DEFAULT_TEST_PASSWORD @@ -29,13 +28,13 @@ MINUTE, SECOND, AutoRegisteredUser, + RestartableWebSocket, RunningState, ServiceType, SocketIOEvent, SocketIOProjectClosedWaiter, SocketIOProjectStateUpdatedWaiter, decode_socketio_42_message, - web_socket_default_log_handler, ) from pytest_simcore.helpers.pydantic_extension import Secret4TestsStr @@ -323,6 +322,11 @@ def _do() -> AutoRegisteredUser: return _do +@pytest.fixture(scope="session") +def store_browser_context() -> bool: + return False + + @pytest.fixture def log_in_and_out( page: Page, @@ -331,7 +335,9 @@ def log_in_and_out( user_password: Secret4TestsStr, auto_register: bool, register: Callable[[], AutoRegisteredUser], -) -> Iterator[WebSocket]: + store_browser_context: bool, + context: BrowserContext, +) -> Iterator[RestartableWebSocket]: with log_context( logging.INFO, f"Open {product_url=} using {user_name=}/{user_password=}/{auto_register=}", @@ -374,8 +380,8 @@ def log_in_and_out( page.get_by_test_id("loginSubmitBtn").click() assert response_info.value.ok, f"{response_info.value.json()}" - ws = ws_info.value - assert not ws.is_closed() + assert not ws_info.value.is_closed() + restartable_wb = RestartableWebSocket.create(page, ws_info.value) # Welcome to Sim4Life page.wait_for_timeout(5000) @@ -389,8 +395,11 @@ def log_in_and_out( if quickStartWindowCloseBtnLocator.is_visible(): quickStartWindowCloseBtnLocator.click() - with web_socket_default_log_handler(ws): - yield ws + if store_browser_context: + context.storage_state(path="state.json") + + # with web_socket_default_log_handler(ws): + yield restartable_wb with log_context( logging.INFO, @@ -408,7 +417,7 @@ def log_in_and_out( @pytest.fixture def create_new_project_and_delete( page: Page, - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, is_product_billable: bool, api_request_context: APIRequestContext, product_url: AnyUrl, @@ -453,7 +462,6 @@ def _( open_button.click() # Open project with default resources open_button = page.get_by_test_id("openWithResources") - time.sleep(2) # wait until the study options are filled up # it returns a Long Running Task with page.expect_response( re.compile(rf"/projects\?from_study\={template_id}") @@ -500,13 +508,11 @@ def wait_for_done(response): if is_product_billable: # Open project with default resources open_button = page.get_by_test_id("openWithResources") - time.sleep(2) # wait until the study options are filled up open_button.click() open_with_resources_clicked = True if is_product_billable and not open_with_resources_clicked: # Open project with default resources open_button = page.get_by_test_id("openWithResources") - time.sleep(2) # wait until the study options are filled up open_button.click() project_data = response_info.value.json() assert project_data @@ -660,7 +666,7 @@ def _( def start_and_stop_pipeline( product_url: AnyUrl, page: Page, - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, api_request_context: APIRequestContext, ) -> Iterator[Callable[[], SocketIOEvent]]: started_pipeline_ids = [] diff --git a/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py b/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py index 6afc2bb1f13..2451f1e3648 100644 --- a/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py +++ b/tests/e2e-playwright/tests/jupyterlabs/test_jupyterlab.py @@ -16,7 +16,12 @@ from playwright.sync_api import Page, WebSocket from pydantic import ByteSize from pytest_simcore.helpers.logging_tools import log_context -from pytest_simcore.helpers.playwright import MINUTE, SECOND, ServiceType +from pytest_simcore.helpers.playwright import ( + MINUTE, + SECOND, + RestartableWebSocket, + ServiceType, +) _WAITING_FOR_SERVICE_TO_START: Final[int] = ( 10 * MINUTE @@ -110,8 +115,11 @@ def test_jupyterlab( iframe.get_by_role("button", name="New Launcher").click() with page.expect_websocket(_JLabWaitForTerminalWebSocket()) as ws_info: iframe.get_by_label("Launcher").get_by_text("Terminal").click() - terminal_web_socket = ws_info.value - assert not terminal_web_socket.is_closed() + + assert not ws_info.value.is_closed() + restartable_terminal_web_socket = RestartableWebSocket.create( + page, ws_info.value + ) terminal = iframe.locator( "#jp-Terminal-0 > div > div.xterm-screen" @@ -122,7 +130,7 @@ def test_jupyterlab( terminal.press("Enter") # NOTE: this call creates a large file with random blocks inside blocks_count = int(large_file_size / large_file_block_size) - with terminal_web_socket.expect_event( + with restartable_terminal_web_socket.expect_event( "framereceived", _JLabTerminalWebSocketWaiter( expected_message_type="stdout", expected_message_contents="copied" diff --git a/tests/e2e-playwright/tests/platform_CI_tests/conftest.py b/tests/e2e-playwright/tests/platform_CI_tests/conftest.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/e2e-playwright/tests/platform_CI_tests/test_platform.py b/tests/e2e-playwright/tests/platform_CI_tests/test_platform.py new file mode 100644 index 00000000000..edcac0fca64 --- /dev/null +++ b/tests/e2e-playwright/tests/platform_CI_tests/test_platform.py @@ -0,0 +1,86 @@ +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument +# pylint: disable=unused-variable +# pylint: disable=too-many-arguments +# pylint: disable=too-many-statements +# pylint: disable=no-name-in-module + +from pathlib import Path + +import pytest + + +@pytest.fixture(scope="session") +def store_browser_context() -> bool: + return True + + +@pytest.fixture +def logged_in_context( + playwright, + store_browser_context: bool, + request: pytest.FixtureRequest, + pytestconfig, +): + is_headed = "--headed" in pytestconfig.invocation_params.args + + file_path = Path("state.json") + if not file_path.exists(): + request.getfixturevalue("log_in_and_out") + + browser = playwright.chromium.launch(headless=not is_headed) + context = browser.new_context(storage_state="state.json") + yield context + context.close() + browser.close() + + +@pytest.fixture(scope="module") +def test_module_teardown(): + + yield # Run the tests + + file_path = Path("state.json") + if file_path.exists(): + file_path.unlink() + + +def test_simple_folder_workflow(logged_in_context, product_url, test_module_teardown): + page = logged_in_context.new_page() + + page.goto(f"{product_url}") + page.wait_for_timeout(1000) + page.get_by_test_id("newFolderButton").click() + + with page.expect_response( + lambda response: "folders" in response.url + and response.status == 201 + and response.request.method == "POST" + ) as response_info: + page.get_by_test_id("folderEditorTitle").fill("My new folder") + page.get_by_test_id("folderEditorCreate").click() + + _folder_id = response_info.value.json()["data"]["folderId"] + page.get_by_test_id(f"folderItem_{_folder_id}").click() + page.get_by_test_id("workspacesAndFoldersTreeItem_null_null").click() + + +def test_simple_workspace_workflow( + logged_in_context, product_url, test_module_teardown +): + page = logged_in_context.new_page() + + page.goto(f"{product_url}") + page.wait_for_timeout(1000) + page.get_by_test_id("workspacesAndFoldersTreeItem_-1_null").click() + + with page.expect_response( + lambda response: "workspaces" in response.url + and response.status == 201 + and response.request.method == "POST" + ) as response_info: + page.get_by_test_id("newWorkspaceButton").click() + page.get_by_test_id("workspaceEditorSave").click() + _workspace_id = response_info.value.json()["data"]["workspaceId"] + page.get_by_test_id(f"workspaceItem_{_workspace_id}").click() + page.get_by_test_id("workspacesAndFoldersTreeItem_null_null").click() diff --git a/tests/e2e-playwright/tests/sim4life/test_sim4life.py b/tests/e2e-playwright/tests/sim4life/test_sim4life.py index 924e6efa535..28b6ecf56a7 100644 --- a/tests/e2e-playwright/tests/sim4life/test_sim4life.py +++ b/tests/e2e-playwright/tests/sim4life/test_sim4life.py @@ -10,12 +10,9 @@ from collections.abc import Callable from typing import Any -from playwright.sync_api import Page, WebSocket +from playwright.sync_api import Page from pydantic import AnyUrl -from pytest_simcore.helpers.playwright import ( - ServiceType, - web_socket_default_log_handler, -) +from pytest_simcore.helpers.playwright import RestartableWebSocket, ServiceType from pytest_simcore.helpers.playwright_sim4life import ( check_video_streaming, interact_with_s4l, @@ -29,7 +26,7 @@ def test_sim4life( [ServiceType, str, str | None], dict[str, Any] ], create_project_from_new_button: Callable[[str], dict[str, Any]], - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, service_key: str, use_plus_button: bool, is_autoscaled: bool, @@ -59,9 +56,8 @@ def test_sim4life( product_url=product_url, ) s4l_websocket = resp["websocket"] - with web_socket_default_log_handler(s4l_websocket): - s4l_iframe = resp["iframe"] - interact_with_s4l(page, s4l_iframe) + s4l_iframe = resp["iframe"] + interact_with_s4l(page, s4l_iframe) - if check_videostreaming: - check_video_streaming(page, s4l_iframe, s4l_websocket) + if check_videostreaming: + check_video_streaming(page, s4l_iframe, s4l_websocket) diff --git a/tests/e2e-playwright/tests/sim4life/test_template.py b/tests/e2e-playwright/tests/sim4life/test_template.py index fb9b260c992..423437e04a4 100644 --- a/tests/e2e-playwright/tests/sim4life/test_template.py +++ b/tests/e2e-playwright/tests/sim4life/test_template.py @@ -10,8 +10,9 @@ from collections.abc import Callable from typing import Any -from playwright.sync_api import Page, WebSocket -from pytest_simcore.helpers.playwright import web_socket_default_log_handler +from playwright.sync_api import Page +from pydantic import AnyUrl +from pytest_simcore.helpers.playwright import RestartableWebSocket from pytest_simcore.helpers.playwright_sim4life import ( check_video_streaming, interact_with_s4l, @@ -22,10 +23,11 @@ def test_template( page: Page, create_project_from_template_dashboard: Callable[[str], dict[str, Any]], - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, template_id: str, is_autoscaled: bool, check_videostreaming: bool, + product_url: AnyUrl, ): project_data = create_project_from_template_dashboard(template_id) @@ -37,12 +39,16 @@ def test_template( assert len(node_ids) == 1, "Expected 1 node in the workbench!" resp = wait_for_launched_s4l( - page, node_ids[0], log_in_and_out, autoscaled=is_autoscaled, copy_workspace=True + page, + node_ids[0], + log_in_and_out, + autoscaled=is_autoscaled, + copy_workspace=True, + product_url=product_url, ) s4l_websocket = resp["websocket"] - with web_socket_default_log_handler(s4l_websocket): - s4l_iframe = resp["iframe"] - interact_with_s4l(page, s4l_iframe) + s4l_iframe = resp["iframe"] + interact_with_s4l(page, s4l_iframe) - if check_videostreaming: - check_video_streaming(page, s4l_iframe, s4l_websocket) + if check_videostreaming: + check_video_streaming(page, s4l_iframe, s4l_websocket) diff --git a/tests/e2e-playwright/tests/sleepers/test_sleepers.py b/tests/e2e-playwright/tests/sleepers/test_sleepers.py index d12953d1ed0..4415fcf3e49 100644 --- a/tests/e2e-playwright/tests/sleepers/test_sleepers.py +++ b/tests/e2e-playwright/tests/sleepers/test_sleepers.py @@ -16,7 +16,7 @@ from packaging.version import Version from packaging.version import parse as parse_version -from playwright.sync_api import Page, WebSocket +from playwright.sync_api import Page from pytest_simcore.helpers.logging_tools import ( ContextMessages, log_context, @@ -24,6 +24,7 @@ ) from pytest_simcore.helpers.playwright import ( MINUTE, + RestartableWebSocket, RunningState, ServiceType, SocketIOEvent, @@ -78,7 +79,7 @@ def _get_file_names(page: Page) -> list[str]: def test_sleepers( page: Page, - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, create_project_from_service_dashboard: Callable[ [ServiceType, str, str | None], dict[str, Any] ], diff --git a/tests/e2e-playwright/tests/tip/test_ti_plan.py b/tests/e2e-playwright/tests/tip/test_ti_plan.py index 56f028d197d..7709071e7ea 100644 --- a/tests/e2e-playwright/tests/tip/test_ti_plan.py +++ b/tests/e2e-playwright/tests/tip/test_ti_plan.py @@ -19,6 +19,7 @@ from pytest_simcore.helpers.playwright import ( MINUTE, SECOND, + RestartableWebSocket, app_mode_trigger_next_app, expected_service_running, wait_for_service_running, @@ -89,7 +90,7 @@ def __call__(self, message: str) -> bool: def test_classic_ti_plan( # noqa: PLR0915 page: Page, - log_in_and_out: WebSocket, + log_in_and_out: RestartableWebSocket, is_autoscaled: bool, is_product_lite: bool, create_tip_plan_from_dashboard: Callable[[str], dict[str, Any]], @@ -209,11 +210,12 @@ def test_classic_ti_plan( # noqa: PLR0915 ti_iframe = service_running.iframe_locator assert ti_iframe - jlab_websocket = ws_info.value + assert not ws_info.value.is_closed() + restartable_jlab_websocket = RestartableWebSocket.create(page, ws_info.value) with ( log_context(logging.INFO, "Run optimization"), - jlab_websocket.expect_event( + restartable_jlab_websocket.expect_event( "framereceived", _JLabWebSocketWaiter( expected_header_msg_type="stream", @@ -228,11 +230,18 @@ def test_classic_ti_plan( # noqa: PLR0915 ) with log_context(logging.INFO, "Create report"): - - ti_iframe.get_by_role("button", name="Load Analysis").click() - page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) - ti_iframe.get_by_role("button", name="Load").nth(1).click() - page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) + with log_context( + logging.INFO, + f"Click button - `Load Analysis` and wait for {_JLAB_REPORTING_MAX_TIME}", + ): + ti_iframe.get_by_role("button", name="Load Analysis").click() + page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) + with log_context( + logging.INFO, + f"Click button - `Load` and wait for {_JLAB_REPORTING_MAX_TIME}", + ): + ti_iframe.get_by_role("button", name="Load").nth(1).click() + page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) if is_product_lite: assert ( @@ -248,14 +257,34 @@ def test_classic_ti_plan( # noqa: PLR0915 ).is_enabled() else: - ti_iframe.get_by_role("button", name="Add to Report (0)").nth(0).click() - page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) - ti_iframe.get_by_role("button", name="Export to S4L").click() - page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) - ti_iframe.get_by_role("button", name="Add to Report (1)").nth(1).click() - page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) - ti_iframe.get_by_role("button", name="Export Report").click() - page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) + with log_context( + logging.INFO, + f"Click button - `Add to Report (0)` and wait for {_JLAB_REPORTING_MAX_TIME}", + ): + ti_iframe.get_by_role("button", name="Add to Report (0)").nth( + 0 + ).click() + page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) + with log_context( + logging.INFO, + f"Click button - `Export to S4L` and wait for {_JLAB_REPORTING_MAX_TIME}", + ): + ti_iframe.get_by_role("button", name="Export to S4L").click() + page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) + with log_context( + logging.INFO, + f"Click button - `Add to Report (1)` and wait for {_JLAB_REPORTING_MAX_TIME}", + ): + ti_iframe.get_by_role("button", name="Add to Report (1)").nth( + 1 + ).click() + page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) + with log_context( + logging.INFO, + f"Click button - `Export Report` and wait for {_JLAB_REPORTING_MAX_TIME}", + ): + ti_iframe.get_by_role("button", name="Export Report").click() + page.wait_for_timeout(_JLAB_REPORTING_MAX_TIME) with log_context(logging.INFO, "Check outputs"): if is_product_lite: diff --git a/tests/e2e/package-lock.json b/tests/e2e/package-lock.json index 3e27937ad5e..d5aaa2c02e9 100644 --- a/tests/e2e/package-lock.json +++ b/tests/e2e/package-lock.json @@ -1975,9 +1975,9 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -7553,9 +7553,9 @@ } }, "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", + "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0",