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",