diff --git a/api/specs/web-server/_catalog.py b/api/specs/web-server/_catalog.py index 900a6018adf6..4902e331f88f 100644 --- a/api/specs/web-server/_catalog.py +++ b/api/specs/web-server/_catalog.py @@ -3,8 +3,8 @@ from fastapi import APIRouter, Depends from models_library.api_schemas_api_server.pricing_plans import ServicePricingPlanGet from models_library.api_schemas_webserver.catalog import ( + CatalogLatestServiceGet, CatalogServiceGet, - CatalogServiceListItem, CatalogServiceUpdate, ServiceInputGet, ServiceInputKey, @@ -34,10 +34,9 @@ @router.get( "/catalog/services/-/latest", - response_model=Page[CatalogServiceListItem], + response_model=Page[CatalogLatestServiceGet], ) -def list_services_latest(_query: Annotated[ListServiceParams, Depends()]): - pass +def list_services_latest(_query: Annotated[ListServiceParams, Depends()]): ... @router.get( diff --git a/packages/models-library/src/models_library/api_schemas_catalog/services.py b/packages/models-library/src/models_library/api_schemas_catalog/services.py index 9e57924808b6..a80490fad36a 100644 --- a/packages/models-library/src/models_library/api_schemas_catalog/services.py +++ b/packages/models-library/src/models_library/api_schemas_catalog/services.py @@ -202,8 +202,15 @@ class _BaseServiceGetV2(CatalogOutputSchema): access_rights: dict[GroupID, ServiceGroupAccessRightsV2] | None - classifiers: list[str] | None = [] - quality: dict[str, Any] = {} + classifiers: Annotated[ + list[str] | None, + Field(default_factory=list), + ] = DEFAULT_FACTORY + + quality: Annotated[ + dict[str, Any], + Field(default_factory=dict), + ] = DEFAULT_FACTORY model_config = ConfigDict( extra="forbid", @@ -212,6 +219,34 @@ class _BaseServiceGetV2(CatalogOutputSchema): ) +class LatestServiceGet(_BaseServiceGetV2): + release: Annotated[ + ServiceRelease, + Field(description="release information of current (latest) service"), + ] + + @staticmethod + def _update_json_schema_extra(schema: JsonDict) -> None: + schema.update( + { + "examples": [ + { + **_EXAMPLE_SLEEPER, # v2.2.1 (latest) + "release": { + "version": _EXAMPLE_SLEEPER["version"], + "version_display": "Summer Release", + "released": "2025-07-20T15:00:00", + }, + } + ] + } + ) + + model_config = ConfigDict( + json_schema_extra=_update_json_schema_extra, + ) + + class ServiceGetV2(_BaseServiceGetV2): # Model used in catalog's rpc and rest interfaces history: Annotated[ @@ -235,7 +270,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None: { "version": _EXAMPLE_SLEEPER["version"], "version_display": "Summer Release", - "released": "2024-07-20T15:00:00", + "released": "2024-07-21T15:00:00", }, { "version": "2.0.0", @@ -263,7 +298,7 @@ def _update_json_schema_extra(schema: JsonDict) -> None: }, { "version": "0.9.0", - "retired": "2024-07-20T15:00:00", + "retired": "2024-07-20T16:00:00", }, {"version": "0.8.0"}, {"version": "0.1.0"}, @@ -288,21 +323,9 @@ def _update_json_schema_extra(schema: JsonDict) -> None: ) -class ServiceListItem(_BaseServiceGetV2): - history: Annotated[ - list[ServiceRelease], - Field( - default_factory=list, - deprecated=True, - description="History will be replaced by current 'release' instead", - json_schema_extra={"default": []}, - ), - ] = DEFAULT_FACTORY - - PageRpcServicesGetV2: TypeAlias = PageRpc[ # WARNING: keep this definition in models_library and not in the RPC interface - ServiceListItem + LatestServiceGet ] ServiceResourcesGet: TypeAlias = ServiceResourcesDict diff --git a/packages/models-library/src/models_library/api_schemas_webserver/catalog.py b/packages/models-library/src/models_library/api_schemas_webserver/catalog.py index 76fdf4ac06ef..7b490ad338e1 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/catalog.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/catalog.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, TypeAlias +from typing import Annotated, TypeAlias from pydantic import ConfigDict, Field from pydantic.config import JsonDict @@ -106,132 +106,131 @@ def _update_json_schema_extra(schema: JsonDict) -> None: ServiceInputsGetDict: TypeAlias = dict[ServicePortKey, ServiceInputGet] ServiceOutputsGetDict: TypeAlias = dict[ServicePortKey, ServiceOutputGet] - - -_EXAMPLE_FILEPICKER: dict[str, Any] = { - **api_schemas_catalog_services.ServiceGet.model_json_schema()["examples"][1], - "inputs": {}, - "outputs": { - "outFile": { - "displayOrder": 0, - "label": "File", - "description": "Chosen File", - "type": "data:*/*", - "fileToKeyMap": None, - "keyId": "outFile", - } - }, -} - -_EXAMPLE_SLEEPER: dict[str, Any] = { - **api_schemas_catalog_services.ServiceGet.model_json_schema()["examples"][0], - "inputs": { - "input_1": { - "displayOrder": 1, - "label": "File with int number", - "description": "Pick a file containing only one integer", - "type": "data:text/plain", - "fileToKeyMap": {"single_number.txt": "input_1"}, - "keyId": "input_1", - }, - "input_2": { - "unitLong": "second", - "unitShort": "s", - "label": "Sleep interval", - "description": "Choose an amount of time to sleep in range [0:]", - "keyId": "input_2", - "displayOrder": 2, - "type": "ref_contentSchema", - "contentSchema": { - "title": "Sleep interval", - "type": "integer", - "x_unit": "second", - "minimum": 0, - }, - "defaultValue": 2, - }, - "input_3": { - "displayOrder": 3, - "label": "Fail after sleep", - "description": "If set to true will cause service to fail after it sleeps", - "type": "boolean", - "defaultValue": False, - "keyId": "input_3", - }, - "input_4": { - "unitLong": "meter", - "unitShort": "m", - "label": "Distance to bed", - "description": "It will first walk the distance to bed", - "keyId": "input_4", - "displayOrder": 4, - "type": "ref_contentSchema", - "contentSchema": { - "title": "Distance to bed", - "type": "integer", - "x_unit": "meter", - }, - "defaultValue": 0, - }, - "input_5": { - "unitLong": "byte", - "unitShort": "B", - "label": "Dream (or nightmare) of the night", - "description": "Defines the size of the dream that will be generated [0:]", - "keyId": "input_5", - "displayOrder": 5, - "type": "ref_contentSchema", - "contentSchema": { - "title": "Dream of the night", - "type": "integer", - "x_unit": "byte", - "minimum": 0, - }, - "defaultValue": 0, - }, - }, - "outputs": { - "output_1": { - "displayOrder": 1, - "label": "File containing one random integer", - "description": "Integer is generated in range [1-9]", - "type": "data:text/plain", - "fileToKeyMap": {"single_number.txt": "output_1"}, - "keyId": "output_1", - }, - "output_2": { - "unitLong": "second", - "unitShort": "s", - "label": "Random sleep interval", - "description": "Interval is generated in range [1-9]", - "keyId": "output_2", - "displayOrder": 2, - "type": "ref_contentSchema", - "contentSchema": { - "title": "Random sleep interval", - "type": "integer", - "x_unit": "second", - }, - }, - "output_3": { - "displayOrder": 3, - "label": "Dream output", - "description": "Contains some random data representing a dream", - "type": "data:text/plain", - "fileToKeyMap": {"dream.txt": "output_3"}, - "keyId": "output_3", - }, - }, -} - - ServiceResourcesGet: TypeAlias = api_schemas_catalog_services.ServiceResourcesGet -class CatalogServiceListItem(api_schemas_catalog_services.ServiceListItem): +class CatalogLatestServiceGet(api_schemas_catalog_services.LatestServiceGet): inputs: ServiceInputsGetDict # type: ignore[assignment] outputs: ServiceOutputsGetDict # type: ignore[assignment] + @staticmethod + def _update_json_schema_extra(schema: JsonDict) -> None: + base_example = ( + api_schemas_catalog_services.LatestServiceGet.model_json_schema()[ + "examples" + ][0] + ) + + schema.update( + { + "example": { + **base_example, + "inputs": { + "input_1": { + "displayOrder": 1, + "label": "File with int number", + "description": "Pick a file containing only one integer", + "type": "data:text/plain", + "fileToKeyMap": {"single_number.txt": "input_1"}, + "keyId": "input_1", + }, + "input_2": { + "unitLong": "second", + "unitShort": "s", + "label": "Sleep interval", + "description": "Choose an amount of time to sleep in range [0:]", + "keyId": "input_2", + "displayOrder": 2, + "type": "ref_contentSchema", + "contentSchema": { + "title": "Sleep interval", + "type": "integer", + "x_unit": "second", + "minimum": 0, + }, + "defaultValue": 2, + }, + "input_3": { + "displayOrder": 3, + "label": "Fail after sleep", + "description": "If set to true will cause service to fail after it sleeps", + "type": "boolean", + "defaultValue": False, + "keyId": "input_3", + }, + "input_4": { + "unitLong": "meter", + "unitShort": "m", + "label": "Distance to bed", + "description": "It will first walk the distance to bed", + "keyId": "input_4", + "displayOrder": 4, + "type": "ref_contentSchema", + "contentSchema": { + "title": "Distance to bed", + "type": "integer", + "x_unit": "meter", + }, + "defaultValue": 0, + }, + "input_5": { + "unitLong": "byte", + "unitShort": "B", + "label": "Dream (or nightmare) of the night", + "description": "Defines the size of the dream that will be generated [0:]", + "keyId": "input_5", + "displayOrder": 5, + "type": "ref_contentSchema", + "contentSchema": { + "title": "Dream of the night", + "type": "integer", + "x_unit": "byte", + "minimum": 0, + }, + "defaultValue": 0, + }, + }, + "outputs": { + "output_1": { + "displayOrder": 1, + "label": "File containing one random integer", + "description": "Integer is generated in range [1-9]", + "type": "data:text/plain", + "fileToKeyMap": {"single_number.txt": "output_1"}, + "keyId": "output_1", + }, + "output_2": { + "unitLong": "second", + "unitShort": "s", + "label": "Random sleep interval", + "description": "Interval is generated in range [1-9]", + "keyId": "output_2", + "displayOrder": 2, + "type": "ref_contentSchema", + "contentSchema": { + "title": "Random sleep interval", + "type": "integer", + "x_unit": "second", + }, + }, + "output_3": { + "displayOrder": 3, + "label": "Dream output", + "description": "Contains some random data representing a dream", + "type": "data:text/plain", + "fileToKeyMap": {"dream.txt": "output_3"}, + "keyId": "output_3", + }, + }, + } + } + ) + + model_config = ConfigDict( + **OutputSchema.model_config, + json_schema_extra=_update_json_schema_extra, + ) + class CatalogServiceGet(api_schemas_catalog_services.ServiceGetV2): # pylint: disable=too-many-ancestors diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py index ae137dcd1b95..ca4f8876f597 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/catalog/services.py @@ -5,9 +5,9 @@ from models_library.api_schemas_catalog import CATALOG_RPC_NAMESPACE from models_library.api_schemas_catalog.services import ( + LatestServiceGet, MyServiceGet, ServiceGetV2, - ServiceListItem, ServiceUpdateV2, ) from models_library.products import ProductName @@ -35,7 +35,7 @@ async def list_services_paginated( # pylint: disable=too-many-arguments user_id: UserID, limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, offset: NonNegativeInt = 0, -) -> PageRpc[ServiceListItem]: +) -> PageRpc[LatestServiceGet]: """ Raises: ValidationError: on invalid arguments @@ -63,9 +63,9 @@ async def _call( product_name=product_name, user_id=user_id, limit=limit, offset=offset ) assert ( # nosec - TypeAdapter(PageRpc[ServiceListItem]).validate_python(result) is not None + TypeAdapter(PageRpc[LatestServiceGet]).validate_python(result) is not None ) - return cast(PageRpc[ServiceListItem], result) + return cast(PageRpc[LatestServiceGet], result) @log_decorator(_logger, level=logging.DEBUG) diff --git a/services/catalog/src/simcore_service_catalog/api/rpc/_services.py b/services/catalog/src/simcore_service_catalog/api/rpc/_services.py index 6d95fbeb962a..7568b18e351b 100644 --- a/services/catalog/src/simcore_service_catalog/api/rpc/_services.py +++ b/services/catalog/src/simcore_service_catalog/api/rpc/_services.py @@ -66,7 +66,7 @@ async def list_services_paginated( ) -> PageRpcServicesGetV2: assert app.state.engine # nosec - total_count, items = await services_api.list_services_paginated( + total_count, items = await services_api.list_latest_services( repo=ServicesRepository(app.state.engine), director_api=get_director_api(app), product_name=product_name, diff --git a/services/catalog/src/simcore_service_catalog/db/repositories/_services_sql.py b/services/catalog/src/simcore_service_catalog/db/repositories/_services_sql.py index bd4828063a67..d83dffbdb220 100644 --- a/services/catalog/src/simcore_service_catalog/db/repositories/_services_sql.py +++ b/services/catalog/src/simcore_service_catalog/db/repositories/_services_sql.py @@ -32,7 +32,7 @@ def list_services_stmt( combine_access_with_and: bool | None = True, product_name: str | None = None, ) -> Select: - stmt = sa.select(SERVICES_META_DATA_COLS) + stmt = sa.select(*SERVICES_META_DATA_COLS) if gids or execute_access or write_access: conditions: list[Any] = [] @@ -135,7 +135,7 @@ def total_count_stmt( ) -def list_latest_services_with_history_stmt( +def list_latest_services_stmt( *, product_name: ProductName, user_id: UserID, @@ -174,7 +174,7 @@ def list_latest_services_with_history_stmt( ) # get all information of latest's services listed in CTE - latest_query = ( + latest_stmt = ( sa.select( services_meta_data.c.key, services_meta_data.c.version, @@ -206,126 +206,26 @@ def list_latest_services_with_history_stmt( .subquery("latest_sq") ) - # get history for every unique service-key in CTE - _accessible_sq = ( - sa.select( - services_meta_data.c.key, - services_meta_data.c.version, - ) - .distinct() - .select_from( - services_meta_data.join( - cte, - services_meta_data.c.key == cte.c.key, - ) - # joins because access-rights might change per version - .join( - services_access_rights, - (services_meta_data.c.key == services_access_rights.c.key) - & (services_meta_data.c.version == services_access_rights.c.version) - & (services_access_rights.c.product_name == product_name), - ) - .join( - user_to_groups, - (user_to_groups.c.gid == services_access_rights.c.gid) - & (user_to_groups.c.uid == user_id), - ) - .outerjoin( - services_compatibility, - (services_meta_data.c.key == services_compatibility.c.key) - & (services_meta_data.c.version == services_compatibility.c.version), - ) - ) - .where(access_rights) - .subquery("accessible_sq") - ) - - history_subquery = ( - sa.select( - services_meta_data.c.key, - services_meta_data.c.version, - services_meta_data.c.version_display, - services_meta_data.c.deprecated, - services_meta_data.c.created, - services_compatibility.c.custom_policy, # CompatiblePolicyDict | None - ) - .select_from( - services_meta_data.join( - _accessible_sq, - (services_meta_data.c.key == _accessible_sq.c.key) - & (services_meta_data.c.version == _accessible_sq.c.version), - ).outerjoin( - services_compatibility, - (services_meta_data.c.key == services_compatibility.c.key) - & (services_meta_data.c.version == services_compatibility.c.version), - ) - ) - .order_by( - services_meta_data.c.key, - sa.desc(_version(services_meta_data.c.version)), # latest version first - ) - .subquery("history_sq") - ) - - return ( - sa.select( - latest_query.c.key, - latest_query.c.version, - # display - latest_query.c.name, - latest_query.c.description, - latest_query.c.description_ui, - latest_query.c.thumbnail, - latest_query.c.icon, - latest_query.c.version_display, - # ownership - latest_query.c.owner_email, - # tags - latest_query.c.classifiers, - latest_query.c.quality, - # lifetime - latest_query.c.created, - latest_query.c.modified, - latest_query.c.deprecated, - # releases (NOTE: at some points we should limit this list?) - array_agg( - func.json_build_object( - "version", - history_subquery.c.version, - "version_display", - history_subquery.c.version_display, - "deprecated", - history_subquery.c.deprecated, - "created", - history_subquery.c.created, - "compatibility_policy", # NOTE: this is the `policy` - history_subquery.c.custom_policy, - ) - ).label("history"), - ) - .join( - history_subquery, - latest_query.c.key == history_subquery.c.key, - ) - .group_by( - history_subquery.c.key, - latest_query.c.key, - latest_query.c.version, - latest_query.c.owner_email, - latest_query.c.name, - latest_query.c.description, - latest_query.c.description_ui, - latest_query.c.thumbnail, - latest_query.c.icon, - latest_query.c.version_display, - latest_query.c.classifiers, - latest_query.c.created, - latest_query.c.modified, - latest_query.c.deprecated, - latest_query.c.quality, - ) - .order_by(history_subquery.c.key) - ) + return sa.select( + latest_stmt.c.key, + latest_stmt.c.version, + # display + latest_stmt.c.name, + latest_stmt.c.description, + latest_stmt.c.description_ui, + latest_stmt.c.thumbnail, + latest_stmt.c.icon, + latest_stmt.c.version_display, + # ownership + latest_stmt.c.owner_email, + # tags + latest_stmt.c.classifiers, + latest_stmt.c.quality, + # lifetime + latest_stmt.c.created, + latest_stmt.c.modified, + latest_stmt.c.deprecated, + ).order_by(latest_stmt.c.key) def can_get_service_stmt( diff --git a/services/catalog/src/simcore_service_catalog/db/repositories/services.py b/services/catalog/src/simcore_service_catalog/db/repositories/services.py index 96433c83dd1d..509a23d68d69 100644 --- a/services/catalog/src/simcore_service_catalog/db/repositories/services.py +++ b/services/catalog/src/simcore_service_catalog/db/repositories/services.py @@ -38,7 +38,7 @@ can_get_service_stmt, get_service_history_stmt, get_service_stmt, - list_latest_services_with_history_stmt, + list_latest_services_stmt, list_services_stmt, total_count_stmt, ) @@ -126,7 +126,7 @@ async def list_service_releases( search_condition &= services_meta_data.c.version.like(f"{major}.%") query = ( - sa.select(SERVICES_META_DATA_COLS) + sa.select(*SERVICES_META_DATA_COLS) .where(search_condition) .order_by(sa.desc(services_meta_data.c.version)) ) @@ -386,7 +386,7 @@ async def list_latest_services( user_id=user_id, access_rights=AccessRightsClauses.can_read, ) - stmt_page = list_latest_services_with_history_stmt( + stmt_page = list_latest_services_stmt( product_name=product_name, user_id=user_id, access_rights=AccessRightsClauses.can_read, @@ -394,7 +394,7 @@ async def list_latest_services( offset=offset, ) - async with self.db_engine.begin() as conn: + async with self.db_engine.connect() as conn: result = await conn.execute(stmt_total) total_count = result.scalar() or 0 @@ -424,7 +424,7 @@ async def list_latest_services( modified=r.modified, deprecated=r.deprecated, # releases - history=r.history, + history=[], # NOTE: for listing we will not add history. Only get service will produce history ) for r in rows ] @@ -446,7 +446,7 @@ async def get_service_history( access_rights=AccessRightsClauses.can_read, service_key=key, ) - async with self.db_engine.begin() as conn: + async with self.db_engine.connect() as conn: result = await conn.execute(stmt_history) row = result.one_or_none() diff --git a/services/catalog/src/simcore_service_catalog/services/function_services.py b/services/catalog/src/simcore_service_catalog/services/function_services.py index 93abd9466f83..7ed546f251b0 100644 --- a/services/catalog/src/simcore_service_catalog/services/function_services.py +++ b/services/catalog/src/simcore_service_catalog/services/function_services.py @@ -20,9 +20,9 @@ def _as_dict(model_instance: ServiceMetaDataPublished) -> dict[str, Any]: def get_function_service(key, version) -> ServiceMetaDataPublished: try: return next( - s - for s in iter_service_docker_data() - if s.key == key and s.version == version + sc + for sc in iter_service_docker_data() + if sc.key == key and sc.version == version ) except StopIteration as err: raise HTTPException( diff --git a/services/catalog/src/simcore_service_catalog/services/manifest.py b/services/catalog/src/simcore_service_catalog/services/manifest.py index bf7c26a6b63f..5cfbb1d961bd 100644 --- a/services/catalog/src/simcore_service_catalog/services/manifest.py +++ b/services/catalog/src/simcore_service_catalog/services/manifest.py @@ -1,4 +1,4 @@ -""" Services Manifest API Documentation +"""Services Manifest API Documentation The `services.manifest` module provides a read-only API to access the services catalog. The term "Manifest" refers to a detailed, finalized list, traditionally used to denote items that are recorded as part of an official inventory or log, emphasizing the immutable nature of the data. @@ -60,7 +60,7 @@ async def get_services_map( # NOTE: functional-services are services w/o associated image services: ServiceMetaDataPublishedDict = { - (s.key, s.version): s for s in iter_service_docker_data() + (sc.key, sc.version): sc for sc in iter_service_docker_data() } for service in services_in_registry: try: diff --git a/services/catalog/src/simcore_service_catalog/services/services_api.py b/services/catalog/src/simcore_service_catalog/services/services_api.py index 11f8d57644a1..843a91fc713c 100644 --- a/services/catalog/src/simcore_service_catalog/services/services_api.py +++ b/services/catalog/src/simcore_service_catalog/services/services_api.py @@ -2,9 +2,9 @@ from contextlib import suppress from models_library.api_schemas_catalog.services import ( + LatestServiceGet, MyServiceGet, ServiceGetV2, - ServiceListItem, ServiceUpdateV2, ) from models_library.groups import GroupID @@ -36,61 +36,96 @@ _logger = logging.getLogger(__name__) -def _db_to_api_model( +def _aggregate( service_db: ServiceWithHistoryDBGet, access_rights_db: list[ServiceAccessRightsAtDB], service_manifest: ServiceMetaDataPublished, - compatibility_map: dict[ServiceVersion, Compatibility | None] | None = None, -) -> ServiceGetV2: - compatibility_map = compatibility_map or {} - - return ServiceGetV2( - key=service_db.key, - version=service_db.version, - name=service_db.name, - thumbnail=HttpUrl(service_db.thumbnail) if service_db.thumbnail else None, - icon=HttpUrl(service_db.icon) if service_db.icon else None, - description=service_db.description, - description_ui=service_db.description_ui, - version_display=service_db.version_display, - service_type=service_manifest.service_type, - contact=service_manifest.contact, - authors=service_manifest.authors, - owner=(service_db.owner_email if service_db.owner_email else None), - inputs=service_manifest.inputs or {}, - outputs=service_manifest.outputs or {}, - boot_options=service_manifest.boot_options, - min_visible_inputs=service_manifest.min_visible_inputs, - access_rights={ +) -> dict: + return { + "key": service_db.key, + "version": service_db.version, + "name": service_db.name, + "thumbnail": HttpUrl(service_db.thumbnail) if service_db.thumbnail else None, + "icon": HttpUrl(service_db.icon) if service_db.icon else None, + "description": service_db.description, + "description_ui": service_db.description_ui, + "version_display": service_db.version_display, + "service_type": service_manifest.service_type, + "contact": service_manifest.contact, + "authors": service_manifest.authors, + "owner": (service_db.owner_email if service_db.owner_email else None), + "inputs": service_manifest.inputs or {}, + "outputs": service_manifest.outputs or {}, + "boot_options": service_manifest.boot_options, + "min_visible_inputs": service_manifest.min_visible_inputs, + "access_rights": { a.gid: ServiceGroupAccessRightsV2.model_construct( execute=a.execute_access, write=a.write_access, ) for a in access_rights_db }, - classifiers=service_db.classifiers, - quality=service_db.quality, - history=[ - ServiceRelease.model_construct( - version=h.version, - version_display=h.version_display, - released=h.created, - retired=h.deprecated, - compatibility=compatibility_map.get(h.version), - ) - for h in service_db.history - ], + "classifiers": service_db.classifiers, + "quality": service_db.quality, + # NOTE: history/release field is removed + } + + +def _to_latest_get_schema( + service_db: ServiceWithHistoryDBGet, + access_rights_db: list[ServiceAccessRightsAtDB], + service_manifest: ServiceMetaDataPublished, +) -> LatestServiceGet: + + assert len(service_db.history) == 0 # nosec + + return LatestServiceGet.model_validate( + { + **_aggregate(service_db, access_rights_db, service_manifest), + "release": ServiceRelease.model_construct( + version=service_db.version, + version_display=service_db.version_display, + released=service_db.created, + retired=service_db.deprecated, + compatibility=None, + ), + } ) -async def list_services_paginated( +def _to_get_schema( + service_db: ServiceWithHistoryDBGet, + access_rights_db: list[ServiceAccessRightsAtDB], + service_manifest: ServiceMetaDataPublished, + compatibility_map: dict[ServiceVersion, Compatibility | None] | None = None, +) -> ServiceGetV2: + compatibility_map = compatibility_map or {} + + return ServiceGetV2.model_validate( + { + **_aggregate(service_db, access_rights_db, service_manifest), + "history": [ + ServiceRelease.model_construct( + version=h.version, + version_display=h.version_display, + released=h.created, + retired=h.deprecated, + compatibility=compatibility_map.get(h.version), + ) + for h in service_db.history + ], + } + ) + + +async def list_latest_services( repo: ServicesRepository, director_api: DirectorApi, product_name: ProductName, user_id: UserID, limit: PageLimitInt | None, offset: NonNegativeInt = 0, -) -> tuple[NonNegativeInt, list[ServiceListItem]]: +) -> tuple[NonNegativeInt, list[LatestServiceGet]]: # defines the order total_count, services = await repo.list_latest_services( @@ -101,7 +136,7 @@ async def list_services_paginated( # injects access-rights access_rights: dict[tuple[str, str], list[ServiceAccessRightsAtDB]] = ( await repo.batch_get_services_access_rights( - ((s.key, s.version) for s in services), product_name=product_name + ((sc.key, sc.version) for sc in services), product_name=product_name ) ) if not access_rights: @@ -113,45 +148,33 @@ async def list_services_paginated( # get manifest of those with access rights got = await manifest.get_batch_services( - [(s.key, s.version) for s in services if access_rights.get((s.key, s.version))], + [ + (sc.key, sc.version) + for sc in services + if access_rights.get((sc.key, sc.version)) + ], director_api, ) service_manifest = { - (s.key, s.version): s for s in got if isinstance(s, ServiceMetaDataPublished) + (sc.key, sc.version): sc + for sc in got + if isinstance(sc, ServiceMetaDataPublished) } items = [ - _db_to_api_model( + _to_latest_get_schema( service_db=sc, access_rights_db=ar, service_manifest=sm, - compatibility_map=cm, ) for sc in services if ( (ar := access_rights.get((sc.key, sc.version))) and (sm := service_manifest.get((sc.key, sc.version))) - and ( - # NOTE: This operation might be resource-intensive. - # It is temporarily implemented on a trial basis. - cm := await evaluate_service_compatibility_map( - repo, - product_name=product_name, - user_id=user_id, - service_release_history=sc.history, - ) - ) ) ] - return total_count, [ - ServiceListItem.model_validate( - { - **it.model_dump(exclude_unset=True, by_alias=True), - } - ) - for it in items - ] + return total_count, items async def get_service( @@ -206,7 +229,7 @@ async def get_service( service_release_history=service.history, ) - return _db_to_api_model(service, access_rights, service_manifest, compatibility_map) + return _to_get_schema(service, access_rights, service_manifest, compatibility_map) async def update_service( diff --git a/services/catalog/tests/unit/test_db_repositories_services_sql.py b/services/catalog/tests/unit/test_db_repositories_services_sql.py index 46993f5a7720..09891d5fe5e1 100644 --- a/services/catalog/tests/unit/test_db_repositories_services_sql.py +++ b/services/catalog/tests/unit/test_db_repositories_services_sql.py @@ -9,7 +9,7 @@ can_get_service_stmt, get_service_history_stmt, get_service_stmt, - list_latest_services_with_history_stmt, + list_latest_services_stmt, total_count_stmt, ) @@ -61,7 +61,7 @@ def _check(func_smt, **kwargs): ) _check( - list_latest_services_with_history_stmt, + list_latest_services_stmt, product_name=product_name, user_id=user_id, access_rights=AccessRightsClauses.can_read, diff --git a/services/catalog/tests/unit/with_dbs/test_api_rpc.py b/services/catalog/tests/unit/with_dbs/test_api_rpc.py index 65b6dd70b11e..830650729baa 100644 --- a/services/catalog/tests/unit/with_dbs/test_api_rpc.py +++ b/services/catalog/tests/unit/with_dbs/test_api_rpc.py @@ -162,8 +162,8 @@ async def test_rpc_catalog_client( assert got.key == service_key assert got.version == service_version - assert got.model_dump() == next( - item.model_dump() + assert got.model_dump(exclude={"history"}) == next( + item.model_dump(exclude={"release"}) for item in page.data if (item.key == service_key and item.version == service_version) ) diff --git a/services/catalog/tests/unit/with_dbs/test_db_repositories.py b/services/catalog/tests/unit/with_dbs/test_db_repositories.py index cf52a79e72b9..e8990527c047 100644 --- a/services/catalog/tests/unit/with_dbs/test_db_repositories.py +++ b/services/catalog/tests/unit/with_dbs/test_db_repositories.py @@ -1,7 +1,9 @@ # pylint: disable=redefined-outer-name # pylint: disable=unused-argument # pylint: disable=unused-variable +# pylint: disable=too-many-arguments +from collections import Counter from collections.abc import Callable from dataclasses import dataclass, field @@ -9,7 +11,6 @@ from models_library.products import ProductName from models_library.users import UserID from packaging import version -from packaging.version import Version from pydantic import EmailStr, HttpUrl, TypeAdapter from simcore_service_catalog.db.repositories.services import ServicesRepository from simcore_service_catalog.models.services_db import ( @@ -309,16 +310,13 @@ async def test_list_all_services_and_history( assert len(services_items) == 1 assert total_count == 1 + # latest assert services_items[0].key == "simcore/services/dynamic/jupyterlab" - history = services_items[0].history - assert len(history) == fake_catalog_with_jupyterlab.expected_services_count + assert services_items[0].version == fake_catalog_with_jupyterlab.expected_latest - # latest, ..., first version - assert history[0].version == fake_catalog_with_jupyterlab.expected_latest - - # check sorted - got_versions = [Version(h.version) for h in history] - assert got_versions == sorted(got_versions, reverse=True) + assert ( + len(services_items[0].history) == 0 + ), "list_latest_service does NOT show history" async def test_listing_with_no_services( @@ -365,7 +363,7 @@ async def test_list_all_services_and_history_with_pagination( assert total_count == num_services for service in services_items: - assert len(service.history) == num_versions_per_service + assert len(service.history) == 0, "Do not show history in listing" assert service.version == expected_latest_version _, services_items = await services_repo.list_latest_services( @@ -374,14 +372,22 @@ async def test_list_all_services_and_history_with_pagination( assert len(services_items) == 2 for service in services_items: - assert len(service.history) == num_versions_per_service + assert len(service.history) == 0, "Do not show history in listing" assert TypeAdapter(EmailStr).validate_python( service.owner_email ), "resolved own'es email" - expected_latest_version = service.history[0].version # latest service is first - assert service.version == expected_latest_version + duplicates = [ + service_key + for service_key, count in Counter( + service.key for service in services_items + ).items() + if count > 1 + ] + assert ( + not duplicates + ), f"list of latest versions of services cannot have duplicates, found: {duplicates}" async def test_get_and_update_service_meta_data( diff --git a/services/catalog/tests/unit/with_dbs/test_services_services_api.py b/services/catalog/tests/unit/with_dbs/test_services_services_api.py index a2d3c2551c7a..7deccf3be151 100644 --- a/services/catalog/tests/unit/with_dbs/test_services_services_api.py +++ b/services/catalog/tests/unit/with_dbs/test_services_services_api.py @@ -125,7 +125,7 @@ async def test_list_services_paginated( assert not mocked_director_service_api["get_service"].called - total_count, page_items = await services_api.list_services_paginated( + total_count, page_items = await services_api.list_latest_services( services_repo, director_client, product_name=target_product, @@ -143,7 +143,6 @@ async def test_list_services_paginated( for item in page_items: assert item.access_rights assert item.owner is not None - assert item.history[0].version == item.version got = await services_api.get_service( services_repo, @@ -154,7 +153,10 @@ async def test_list_services_paginated( service_version=item.version, ) - assert got.model_dump() == item.model_dump() + assert got.model_dump(exclude={"history"}) == item.model_dump( + exclude={"release"} + ) + assert item.release in got.history # since it is cached, it should only call it `limit` times assert mocked_director_service_api["get_service"].call_count == limit diff --git a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js index a06ec485d48b..a88202766c08 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/CardBase.js @@ -373,7 +373,14 @@ qx.Class.define("osparc.dashboard.CardBase", { workbench: { check: "Object", nullable: true, - apply: "__applyWorkbench" + }, + + services: { + check: "Array", + init: true, + nullable: false, + apply: "__applyServices", + event: "changeServices", }, uiMode: { @@ -484,13 +491,11 @@ qx.Class.define("osparc.dashboard.CardBase", { uuid = resourceData.uuid ? resourceData.uuid : null; owner = resourceData.prjOwner ? resourceData.prjOwner : ""; workbench = resourceData.workbench ? resourceData.workbench : {}; - icon = osparc.study.Utils.guessIcon(resourceData); break; case "template": uuid = resourceData.uuid ? resourceData.uuid : null; owner = resourceData.prjOwner ? resourceData.prjOwner : ""; workbench = resourceData.workbench ? resourceData.workbench : {}; - icon = osparc.study.Utils.guessIcon(resourceData); break; case "service": uuid = resourceData.key ? resourceData.key : null; @@ -519,6 +524,23 @@ qx.Class.define("osparc.dashboard.CardBase", { hits: resourceData.hits ? resourceData.hits : defaultHits, workbench }); + + if (resourceData["resourceType"] === "study" || resourceData["resourceType"] === "template") { + const params = { + url: { + studyId: this.getResourceData()["uuid"] + } + }; + osparc.data.Resources.fetch("studies", "getServices", params) + .then(resp => { + const services = resp["services"]; + resourceData["services"] = services; + this.setServices(services); + }); + + osparc.study.Utils.guessIcon(resourceData) + .then(iconSource => this.setIcon(iconSource)); + } }, __applyMultiSelectionMode: function(value) { @@ -634,39 +656,31 @@ qx.Class.define("osparc.dashboard.CardBase", { } }, - __applyWorkbench: function(workbench) { - if (workbench === null) { - // it is a service - return; - } - - if (this.isResourceType("study") || this.isResourceType("template")) { - this.setEmptyWorkbench(Object.keys(workbench).length === 0); - } + __applyServices: function(services) { + this.setEmptyWorkbench(services.length === 0); // Updatable study - if (osparc.study.Utils.isWorkbenchRetired(workbench)) { + if (osparc.study.Utils.anyServiceRetired(services)) { this.setUpdatable("retired"); - } else if (osparc.study.Utils.isWorkbenchDeprecated(workbench)) { + } else if (osparc.study.Utils.anyServiceDeprecated(services)) { this.setUpdatable("deprecated"); - } else { - const updatable = osparc.study.Utils.isWorkbenchUpdatable(workbench) - if (updatable) { - this.setUpdatable("updatable"); - } + } else if (osparc.study.Utils.anyServiceUpdatable(services)) { + this.setUpdatable("updatable"); } // Block card - const unaccessibleServices = osparc.study.Utils.getInaccessibleServices(workbench) + const unaccessibleServices = osparc.study.Utils.getCantExecuteServices(services); if (unaccessibleServices.length) { this.setBlocked("UNKNOWN_SERVICES"); - let image = "@FontAwesome5Solid/ban/"; - let toolTipText = this.tr("Service info missing"); + const image = "@FontAwesome5Solid/ban/"; + let toolTipText = this.tr("Unaccessible service(s):"); unaccessibleServices.forEach(unSrv => { - toolTipText += "
" + unSrv.key + ":" + unSrv.version; + toolTipText += "
" + unSrv.key + ":" + osparc.service.Utils.extractVersionDisplay(unSrv.release); }); this.__showBlockedCard(image, toolTipText); } + + this.evaluateMenuButtons(); }, __applyEmptyWorkbench: function(isEmpty) { @@ -953,9 +967,14 @@ qx.Class.define("osparc.dashboard.CardBase", { } }, - __openMoreOptions: function() { + __openResourceDetails: function(openWindowCB) { const resourceData = this.getResourceData(); const resourceDetails = new osparc.dashboard.ResourceDetails(resourceData); + resourceDetails.addListenerOnce("pagesAdded", () => { + if (openWindowCB in resourceDetails) { + resourceDetails[openWindowCB](); + } + }) const win = osparc.dashboard.ResourceDetails.popUpInWindow(resourceDetails); [ "updateStudy", @@ -987,28 +1006,23 @@ qx.Class.define("osparc.dashboard.CardBase", { }, openBilling: function() { - const moreOpts = this.__openMoreOptions(); - moreOpts.openBillingSettings(); + this.__openResourceDetails("openBillingSettings"); }, openAccessRights: function() { - const moreOpts = this.__openMoreOptions(); - moreOpts.openAccessRights(); + this.__openResourceDetails("openAccessRights"); }, openTags: function() { - const moreOpts = this.__openMoreOptions(); - moreOpts.openTags(); + this.__openResourceDetails("openTags"); }, __openQualityEditor: function() { - const moreOpts = this.__openMoreOptions(); - moreOpts.openQuality(); + this.__openResourceDetails("openQuality"); }, __openUpdateServices: function() { - const moreOpts = this.__openMoreOptions(); - moreOpts.openUpdateServices(); + this.__openResourceDetails("openUpdateServices"); }, _getEmptyWorkbenchIcon: function() { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonBase.js b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonBase.js index b6f2b211bda4..5b9e19f184ff 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/GridButtonBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/GridButtonBase.js @@ -297,9 +297,9 @@ qx.Class.define("osparc.dashboard.GridButtonBase", { }); } else { let source = osparc.product.Utils.getThumbnailUrl(); - fetch(value, { method: "HEAD" }) - .then(response => { - if (response.ok) { + osparc.utils.Utils.checkImageExists(value) + .then(exists => { + if (exists) { source = value; } }) diff --git a/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js b/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js index 64ec45195b01..d0a107114bcd 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js @@ -314,24 +314,14 @@ qx.Class.define("osparc.dashboard.NewPlusMenu", { menuButton.setEnabled(false); const key = newStudyData["expectedKey"]; - // Include deprecated versions, they should all be updatable to a non deprecated version - const versions = osparc.service.Utils.getVersions(key, false); - if (versions.length && newStudyData) { - // scale to latest compatible - const latestVersion = versions[0]; - const latestCompatible = osparc.service.Utils.getLatestCompatible(key, latestVersion); - osparc.store.Services.getService(latestCompatible["key"], latestCompatible["version"]) - .then(latestMetadata => { - // make sure this one is not deprecated - if (osparc.service.Utils.isDeprecated(latestMetadata)) { - return; - } - menuButton.setEnabled(true); - this.__addIcon(menuButton, newStudyData, latestMetadata); - this.__addFromResourceButton(menuButton, newStudyData["category"]); - addListenerToButton(menuButton, latestMetadata); - }); + const latestMetadata = osparc.store.Services.getLatest(key); + if (!latestMetadata) { + return; } + menuButton.setEnabled(true); + this.__addIcon(menuButton, newStudyData, latestMetadata); + this.__addFromResourceButton(menuButton, newStudyData["category"]); + addListenerToButton(menuButton, latestMetadata); } else if ("myMostUsed" in newStudyData) { const excludeFrontend = true; const excludeDeprecated = true @@ -343,7 +333,7 @@ qx.Class.define("osparc.dashboard.NewPlusMenu", { }); for (let i=0; i 0) { + if (latestMetadata && latestMetadata["hits"] > 0) { const menuButton = new qx.ui.menu.Button().set({ label: latestMetadata["name"], font: "text-16", diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js index 2ad6467669a1..7de8c51a1179 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceBrowserBase.js @@ -555,7 +555,9 @@ qx.Class.define("osparc.dashboard.ResourceBrowserBase", { }, _getOpenMenuButton: function(resourceData) { - const openButton = new qx.ui.menu.Button(this.tr("Open")); + const studyAlias = osparc.product.Utils.getStudyAlias({firstUpperCase: true}); + const openText = (resourceData["resourceType"] === "study") ? this.tr("Open") : this.tr("New") + " " + studyAlias; + const openButton = new qx.ui.menu.Button(openText); openButton["openResourceButton"] = true; openButton.addListener("execute", () => { switch (resourceData["resourceType"]) { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js index 9d0cb2d1d3cf..a0ecca56e4db 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceDetails.js @@ -23,29 +23,57 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { this.__resourceData = resourceData; - this.__resourceModel = null; + let latestPromise = null; switch (resourceData["resourceType"]) { case "study": - case "template": - this.__resourceModel = new osparc.data.model.Study(resourceData); + case "template": { + const params = { + url: { + "studyId": resourceData["uuid"] + } + }; + latestPromise = osparc.data.Resources.fetch("studies", "getOne", params); break; - case "service": - this.__resourceModel = new osparc.data.model.Service(resourceData); + } + case "service": { + latestPromise = osparc.store.Services.getService(resourceData["key"], resourceData["version"]); break; + } } - this.__resourceModel["resourceType"] = resourceData["resourceType"]; - this.__addPages(); + latestPromise + .then(latestResourceData => { + this.__resourceData = latestResourceData; + this.__resourceData["resourceType"] = resourceData["resourceType"]; + switch (resourceData["resourceType"]) { + case "study": + case "template": { + osparc.store.Services.getStudyServicesMetadata(latestResourceData) + .then(() => { + this.__resourceModel = new osparc.data.model.Study(latestResourceData); + this.__resourceModel["resourceType"] = resourceData["resourceType"]; + this.__addPages(); + }) + break; + } + case "service": { + this.__resourceModel = new osparc.data.model.Service(latestResourceData); + this.__resourceModel["resourceType"] = resourceData["resourceType"]; + this.__addPages(); + break; + } + } + }); }, events: { - "openStudy": "qx.event.type.Data", + "pagesAdded": "qx.event.type.Event", "openTemplate": "qx.event.type.Data", "openService": "qx.event.type.Data", "updateStudy": "qx.event.type.Data", "updateTemplate": "qx.event.type.Data", "updateService": "qx.event.type.Data", - "publishTemplate": "qx.event.type.Data" + "publishTemplate": "qx.event.type.Data", }, @@ -163,10 +191,15 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { "studyId": this.__resourceData["uuid"] } }; - osparc.data.Resources.getOne("studies", params) - .then(updatedStudyData => { + Promise.all([ + osparc.data.Resources.fetch("studies", "getOne", params), + osparc.data.Resources.fetch("studies", "getServices", params) + ]) + .then(values => { + const updatedStudyData = values[0]; + const studyServices = values[1]; openButton.setFetching(false); - const updatableServices = osparc.metadata.ServicesInStudyUpdate.updatableNodeIds(updatedStudyData.workbench); + const updatableServices = osparc.metadata.ServicesInStudyUpdate.updatableNodeIds(updatedStudyData.workbench, studyServices["services"]); if (updatableServices.length && osparc.data.model.Study.canIWrite(updatedStudyData["accessRights"])) { this.__confirmUpdate(); } else { @@ -254,34 +287,31 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { hBox.add(versionsBox); - const versions = osparc.service.Utils.getVersions(this.__resourceData["key"]); - let selectedItem = null; - - // first setSelection - versions.forEach(version => { - selectedItem = osparc.service.Utils.versionToListItem(this.__resourceData["key"], version); - versionsBox.add(selectedItem); - if (this.__resourceData["version"] === version) { - versionsBox.setSelection([selectedItem]); - } - }); - osparc.utils.Utils.growSelectBox(versionsBox, 200); - - // then listen to changes - versionsBox.addListener("changeSelection", e => { - const selection = e.getData(); - if (selection.length) { - const serviceVersion = selection[0].version; - if (serviceVersion !== this.__resourceData["version"]) { - osparc.store.Services.getService(this.__resourceData["key"], serviceVersion) - .then(serviceData => { - serviceData["resourceType"] = "service"; - this.__resourceData = serviceData; - this.__addPages(); - }); + osparc.store.Services.populateVersionsSelectBox(this.__resourceData["key"], versionsBox) + .then(() => { + // first setSelection + const versionFound = versionsBox.getSelectables().find(selectable => selectable.version === this.__resourceData["version"]); + if (versionFound) { + versionsBox.setSelection([versionFound]); } - } - }, this); + osparc.utils.Utils.growSelectBox(versionsBox, 200); + + // then listen to changes + versionsBox.addListener("changeSelection", e => { + const selection = e.getData(); + if (selection.length) { + const serviceVersion = selection[0].version; + if (serviceVersion !== this.__resourceData["version"]) { + osparc.store.Services.getService(this.__resourceData["key"], serviceVersion) + .then(serviceData => { + serviceData["resourceType"] = "service"; + this.__resourceData = serviceData; + this.__addPages(); + }); + } + } + }, this); + }); return hBox; }, @@ -346,6 +376,8 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { tabsView.setSelection([pageFound]); } } + + this.fireEvent("pagesAdded"); }, __getInfoPage: function() { @@ -448,7 +480,7 @@ qx.Class.define("osparc.dashboard.ResourceDetails", { if ( osparc.utils.Resources.isService(resourceData) || !osparc.product.Utils.showStudyPreview() || - !osparc.study.Utils.getUiMode(resourceData) === "workbench" + !(osparc.study.Utils.getUiMode(resourceData) === "workbench") ) { // there is no pipelining or don't show it return null; diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ServiceBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/ServiceBrowser.js index fec523a23413..eb80bd3b7e65 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ServiceBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ServiceBrowser.js @@ -48,9 +48,24 @@ qx.Class.define("osparc.dashboard.ServiceBrowser", { // overridden initResources: function() { this._resourcesList = []; - this.getChildControl("resources-layout"); - this.reloadResources(); - this._hideLoadingPage(); + osparc.store.Services.getServicesLatest(false) + .then(services => { + // Show "Contact Us" message if services.length === 0 + // Most probably is a product-stranger user (it can also be that the catalog is down) + if (Object.keys(services).length === 0) { + let msg = this.tr("It seems you don't have access to this product."); + msg += "
"; + msg += this.tr("Please contact us:"); + msg += "
"; + const supportEmail = osparc.store.VendorInfo.getInstance().getSupportEmail(); + msg += supportEmail; + osparc.FlashMessenger.getInstance().logAs(msg, "WARNING"); + } + + this.getChildControl("resources-layout"); + this.reloadResources(); + this._hideLoadingPage(); + }); }, reloadResources: function() { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js index 9ada537f7509..6b2f783b6103 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/StudyBrowser.js @@ -269,28 +269,6 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { } }); - // Show "Contact Us" message if services.length === 0 - // Most probably is a product-stranger user (it can also be that the catalog is down) - osparc.store.Services.getServicesLatest() - .then(services => { - if (Object.keys(services).length === 0) { - const noAccessText = new qx.ui.basic.Label().set({ - selectable: true, - rich: true, - font: "text-18", - paddingTop: 20 - }); - let msg = this.tr("It seems you don't have access to this product."); - msg += "
"; - msg += "
"; - msg += this.tr("Please contact us:"); - msg += "
"; - const supportEmail = osparc.store.VendorInfo.getInstance().getSupportEmail(); - noAccessText.setValue(msg + supportEmail); - this._addToLayout(noAccessText); - } - }); - this._loadingResourcesBtn.setFetching(true); this._loadingResourcesBtn.setVisibility("visible"); this.__getNextStudiesRequest() @@ -383,7 +361,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { studyId } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(studyData => { this.__studyStateReceived(study["uuid"], studyData["state"]); }); @@ -975,30 +953,20 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { __addNewStudyFromServiceButton: function(newStudyData) { if ("expectedKey" in newStudyData) { const key = newStudyData["expectedKey"]; - // Include deprecated versions, they should all be updatable to a non deprecated version - const versions = osparc.service.Utils.getVersions(key, false); - if (versions.length && newStudyData) { - // scale to latest compatible - const latestVersion = versions[0]; - const latestCompatible = osparc.service.Utils.getLatestCompatible(key, latestVersion); - osparc.store.Services.getService(latestCompatible["key"], latestCompatible["version"]) - .then(latestMetadata => { - // make sure this one is not deprecated - if (osparc.service.Utils.isDeprecated(latestMetadata)) { - return; - } - const title = newStudyData.title + " " + osparc.service.Utils.extractVersionDisplay(latestMetadata); - const desc = newStudyData.description; - const mode = this._resourcesContainer.getMode(); - const newStudyFromServiceButton = (mode === "grid") ? new osparc.dashboard.GridButtonNew(title, desc) : new osparc.dashboard.ListButtonNew(title, desc); - newStudyFromServiceButton.setCardKey("new-"+key); - if (newStudyData["idToWidget"]) { - osparc.utils.Utils.setIdToWidget(newStudyFromServiceButton, newStudyData["idToWidget"]); - } - newStudyFromServiceButton.addListener("tap", () => this.__newStudyFromServiceBtnClicked(latestMetadata["key"], latestMetadata["version"], newStudyData.newStudyLabel)); - this._resourcesContainer.addNonResourceCard(newStudyFromServiceButton); - }) + const latestMetadata = osparc.store.Services.getLatest(key); + if (!latestMetadata) { + return; + } + const title = newStudyData.title + " " + osparc.service.Utils.extractVersionDisplay(latestMetadata); + const desc = newStudyData.description; + const mode = this._resourcesContainer.getMode(); + const newStudyFromServiceButton = (mode === "grid") ? new osparc.dashboard.GridButtonNew(title, desc) : new osparc.dashboard.ListButtonNew(title, desc); + newStudyFromServiceButton.setCardKey("new-"+key); + if (newStudyData["idToWidget"]) { + osparc.utils.Utils.setIdToWidget(newStudyFromServiceButton, newStudyData["idToWidget"]); } + newStudyFromServiceButton.addListener("tap", () => this.__newStudyFromServiceBtnClicked(latestMetadata["key"], latestMetadata["version"], newStudyData.newStudyLabel)); + this._resourcesContainer.addNonResourceCard(newStudyFromServiceButton); } else if ("myMostUsed" in newStudyData) { const excludeFrontend = true; const excludeDeprecated = true @@ -2079,7 +2047,7 @@ qx.Class.define("osparc.dashboard.StudyBrowser", { "studyId": data["data"]["uuid"] } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(studyData => this._updateStudyData(studyData)) .catch(err => { console.error(err); 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 716fa31a2e84..0996efeb7bb9 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/TemplateBrowser.js @@ -326,7 +326,7 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { const templatePromises = []; for (const nodeId in studyData["workbench"]) { const node = studyData["workbench"][nodeId]; - const latestCompatible = osparc.service.Utils.getLatestCompatible(node["key"], node["version"]); + const latestCompatible = osparc.store.Services.getLatestCompatible(node["key"], node["version"]); if (latestCompatible && (node["key"] !== latestCompatible["key"] || node["version"] !== latestCompatible["version"])) { const patchData = {}; if (node["key"] !== latestCompatible["key"]) { @@ -390,7 +390,7 @@ qx.Class.define("osparc.dashboard.TemplateBrowser", { return null; } - const editButton = new qx.ui.menu.Button(this.tr("Edit")); + const editButton = new qx.ui.menu.Button(this.tr("Open")); editButton.addListener("execute", () => this.__editTemplate(templateData), this); return editButton; }, diff --git a/services/static-webserver/client/source/class/osparc/data/Resources.js b/services/static-webserver/client/source/class/osparc/data/Resources.js index 8fcc7a4b005f..42ca4daf86f4 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -125,6 +125,10 @@ qx.Class.define("osparc.data.Resources", { method: "GET", url: statics.API + "/projects/{studyId}" }, + getServices: { + method: "GET", + url: statics.API + "/projects/{studyId}/nodes/-/services" + }, getActive: { useCache: false, method: "GET", @@ -574,24 +578,20 @@ qx.Class.define("osparc.data.Resources", { }, /* - * SERVICES V2 (web-api >=0.42.0) + * SERVICES V2 */ "servicesV2": { useCache: false, // handled in osparc.store.Services idField: ["key", "version"], endpoints: { - get: { + getOne: { method: "GET", - url: statics.API + "/catalog/services/-/latest" + url: statics.API + "/catalog/services/{key}/{version}" }, getPage: { method: "GET", url: statics.API + "/catalog/services/-/latest?offset={offset}&limit={limit}" }, - getOne: { - method: "GET", - url: statics.API + "/catalog/services/{key}/{version}" - }, patch: { method: "PATCH", url: statics.API + "/catalog/services/{key}/{version}" 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 1a0b2d8378d0..efbc869bdf90 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 @@ -480,13 +480,15 @@ qx.Class.define("osparc.data.model.Node", { } } - this.__initLogger(); - - this.initIframeHandler(); - if (this.isParameter()) { this.__initParameter(); } + + if (osparc.store.Store.getInstance().getCurrentStudy()) { + // do not initialize the logger and iframe if the study isn't open + this.__initLogger(); + this.initIframeHandler(); + } }, populateNodeUIData: function(nodeUIData) { @@ -1205,7 +1207,7 @@ qx.Class.define("osparc.data.model.Node", { if (!["int"].includes(type)) { return; } - const newMetadata = osparc.service.Utils.getParameterMetadata("integer"); + const newMetadata = osparc.store.Services.getParameterMetadata("integer"); if (newMetadata) { const value = this.__getInputData()["linspace_start"]; const label = this.getLabel(); @@ -1222,7 +1224,7 @@ qx.Class.define("osparc.data.model.Node", { if (!["int"].includes(type)) { return; } - const metadata = osparc.service.Utils.getLatest("simcore/services/frontend/data-iterator/int-range") + const metadata = osparc.store.Services.getLatest("simcore/services/frontend/data-iterator/int-range") if (metadata) { const value = this.__getOutputData("out_1"); const label = this.getLabel(); 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 d442cb03b63c..68006af83297 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 @@ -426,7 +426,7 @@ qx.Class.define("osparc.data.model.Workbench", { }, __filePickerNodeRequested: async function(nodeId, portId, file) { - const filePickerMetadata = osparc.service.Utils.getFilePicker(); + const filePickerMetadata = osparc.store.Services.getFilePicker(); const filePicker = await this.createNode(filePickerMetadata["key"], filePickerMetadata["version"]); if (filePicker === null) { return; @@ -469,7 +469,7 @@ qx.Class.define("osparc.data.model.Workbench", { // create a new ParameterNode const type = osparc.utils.Ports.getPortType(requesterNode.getMetaData()["inputs"], portId); - const parameterMetadata = osparc.service.Utils.getParameterMetadata(type); + const parameterMetadata = osparc.store.Services.getParameterMetadata(type); if (parameterMetadata) { const parameterNode = await this.createNode(parameterMetadata["key"], parameterMetadata["version"]); if (parameterNode === null) { @@ -499,7 +499,7 @@ qx.Class.define("osparc.data.model.Workbench", { // create a new ProbeNode const requesterPortMD = requesterNode.getMetaData()["outputs"][portId]; const type = osparc.utils.Ports.getPortType(requesterNode.getMetaData()["outputs"], portId); - const probeMetadata = osparc.service.Utils.getProbeMetadata(type); + const probeMetadata = osparc.store.Services.getProbeMetadata(type); if (probeMetadata) { const probeNode = await this.createNode(probeMetadata["key"], probeMetadata["version"]); if (probeNode === null) { diff --git a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js index 1737f1b4bce7..b1cadb8fc9b4 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPage.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPage.js @@ -308,7 +308,7 @@ qx.Class.define("osparc.desktop.MainPage", { "studyId": studyId } }; - osparc.data.Resources.getOne("studies", params2) + osparc.data.Resources.fetch("studies", "getOne", params2) .then(studyData => { if (!studyData) { const msg = this.tr("Study not found"); @@ -348,7 +348,7 @@ qx.Class.define("osparc.desktop.MainPage", { } }; // OM TODO. DO NOT ADD ITERATIONS TO STUDIES CACHE - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(studyData => { if (!studyData) { const msg = this.tr("Iteration not found"); 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 d03f9bc85e27..a8bf54dac246 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/MainPageHandler.js +++ b/services/static-webserver/client/source/class/osparc/desktop/MainPageHandler.js @@ -69,7 +69,7 @@ qx.Class.define("osparc.desktop.MainPageHandler", { "studyId": studyId } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(studyData => { if (!studyData) { const msg = qx.locale.Manager.tr("Study not found"); @@ -110,15 +110,19 @@ qx.Class.define("osparc.desktop.MainPageHandler", { this.setLoadingPageHeader(qx.locale.Manager.tr("Loading ") + studyData.name); this.showLoadingPage(); - const inaccessibleServices = osparc.study.Utils.getInaccessibleServices(studyData["workbench"]) - if (inaccessibleServices.length) { - const msg = osparc.study.Utils.getInaccessibleServicesMsg(inaccessibleServices, studyData["workbench"]); - osparc.FlashMessenger.getInstance().logAs(msg, "ERROR"); - this.showDashboard(); - return; - } - this.showStudyEditor(); - this.__studyEditor.setStudyData(studyData); + + osparc.store.Services.getStudyServicesMetadata(studyData) + .finally(() => { + const inaccessibleServices = osparc.store.Services.getInaccessibleServices(studyData["workbench"]) + if (inaccessibleServices.length) { + const msg = osparc.store.Services.getInaccessibleServicesMsg(inaccessibleServices, studyData["workbench"]); + osparc.FlashMessenger.getInstance().logAs(msg, "ERROR"); + this.showDashboard(); + return; + } + this.showStudyEditor(); + this.__studyEditor.setStudyData(studyData); + }); } } }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js index bcf31c70f556..f741e3806e66 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/StudyEditor.js @@ -134,7 +134,7 @@ qx.Class.define("osparc.desktop.StudyEditor", { "studyId": studyData.uuid } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(latestStudyData => { const study = new osparc.data.model.Study(latestStudyData); this.setStudy(study); diff --git a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js index 1281742898e4..2f40fee70b64 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -1015,7 +1015,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { // HEADER const nodeMetadata = node.getMetaData(); - const version = osparc.service.Utils.getVersionDisplay(nodeMetadata["key"], nodeMetadata["version"]); + const version = osparc.store.Services.getVersionDisplay(nodeMetadata["key"], nodeMetadata["version"]); const header = new qx.ui.basic.Label(`${nodeMetadata["name"]} ${version}`).set({ paddingLeft: 5 }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js index ab25f0207173..0e5181f56317 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/CreditsServiceListItem.js @@ -31,7 +31,7 @@ qx.Class.define("osparc.desktop.credits.CreditsServiceListItem", { const icon = this.getChildControl("icon"); const name = this.getChildControl("title"); - const serviceMetadata = osparc.service.Utils.getLatest(serviceKey); + const serviceMetadata = osparc.store.Services.getLatest(serviceKey); if (serviceMetadata) { const source = osparc.utils.Utils.getIconFromResource(serviceMetadata); icon.setSource(source); diff --git a/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js b/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js index ccd205a92e05..71c8c6e7cbc0 100644 --- a/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js +++ b/services/static-webserver/client/source/class/osparc/form/renderer/PropForm.js @@ -84,7 +84,7 @@ qx.Class.define("osparc.form.renderer.PropForm", { isFieldParametrizable: function(field) { const supportedTypes = []; - const paramsMD = osparc.service.Utils.getParametersMetadata(); + const paramsMD = osparc.store.Services.getParametersMetadata(); paramsMD.forEach(paramMD => { supportedTypes.push(osparc.node.ParameterEditor.getParameterOutputTypeFromMD(paramMD)); }); diff --git a/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js b/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js index e4a784243ee1..7e4710bcbe21 100644 --- a/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js +++ b/services/static-webserver/client/source/class/osparc/info/ServiceUtils.js @@ -64,14 +64,14 @@ qx.Class.define("osparc.info.ServiceUtils", { }, createVersionDisplay: function(key, version) { - const versionDisplay = osparc.service.Utils.getVersionDisplay(key, version); + const versionDisplay = osparc.store.Services.getVersionDisplay(key, version); const label = new qx.ui.basic.Label(versionDisplay); osparc.utils.Utils.setIdToWidget(label, "serviceVersion"); return label; }, createReleasedDate: function(key, version) { - const releasedDate = osparc.service.Utils.getReleasedDate(key, version); + const releasedDate = osparc.store.Services.getReleasedDate(key, version); if (releasedDate) { const label = new qx.ui.basic.Label(); label.set({ diff --git a/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudy.js b/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudy.js index b760ed077767..ad84337b22bf 100644 --- a/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudy.js +++ b/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudy.js @@ -45,6 +45,7 @@ qx.Class.define("osparc.metadata.ServicesInStudy", { const servicesInStudy = osparc.study.Utils.extractUniqueServices(this._studyData["workbench"]); if (servicesInStudy.length) { const promises = []; + // the following calls make sure the history of each service is there servicesInStudy.forEach(srv => promises.push(osparc.store.Services.getService(srv.key, srv.version))); Promise.all(promises) .then(() => this._populateLayout()); diff --git a/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudyUpdate.js b/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudyUpdate.js index bbd9685bfecb..6831b3d70e5f 100644 --- a/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudyUpdate.js +++ b/services/static-webserver/client/source/class/osparc/metadata/ServicesInStudyUpdate.js @@ -27,52 +27,18 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { UPDATE_BUTTON: Object.keys(osparc.metadata.ServicesInStudy.GRID_POS).length+2 }, - anyServiceDeprecated: function(studyData) { - if ("workbench" in studyData) { - return osparc.study.Utils.isWorkbenchDeprecated(studyData["workbench"]); - } - return false; - }, - - anyServiceRetired: function(studyData) { - if ("workbench" in studyData) { - return osparc.study.Utils.isWorkbenchRetired(studyData["workbench"]); - } - return false; - }, - - anyServiceInaccessible: function(studyData) { - if ("workbench" in studyData) { - const inaccessibles = osparc.study.Utils.getInaccessibleServices(studyData["workbench"]); - return inaccessibles.length; - } - return false; - }, - - updatableNodeIds: function(workbench) { + updatableNodeIds: function(workbench, studyServices) { const nodeIds = []; for (const nodeId in workbench) { const node = workbench[nodeId]; - if (osparc.service.Utils.isUpdatable(node)) { + const studyServiceFound = studyServices.find(studyService => studyService["key"] === node["key"] && studyService["release"]["version"] === node["version"]); + if (studyServiceFound && studyServiceFound["release"] && studyServiceFound["release"]["compatibility"]) { nodeIds.push(nodeId); } } return nodeIds; }, - getLatestVersion: function(studyData, nodeId) { - if (nodeId in studyData["workbench"]) { - const node = studyData["workbench"][nodeId]; - if (osparc.service.Utils.isUpdatable(node)) { - const latestCompatible = osparc.service.Utils.getLatestCompatible(node["key"], node["version"]); - if (latestCompatible["version"] !== node["version"]) { - return latestCompatible["version"]; - } - } - } - return null; - }, - colorVersionLabel: function(versionLabel, metadata) { const isDeprecated = osparc.service.Utils.isDeprecated(metadata); const isRetired = osparc.service.Utils.isRetired(metadata); @@ -97,57 +63,58 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { _populateIntroText: async function() { const canIWrite = osparc.data.model.Study.canIWrite(this._studyData["accessRights"]); - const labels = []; - if (this.self().anyServiceInaccessible(this._studyData)) { - const inaccessibleText = this.tr("Some services' information is not accessible. Please contact service owner:"); - const inaccessibleLabel = new qx.ui.basic.Label(inaccessibleText); - labels.push(inaccessibleLabel); - } - if (this.self().anyServiceDeprecated(this._studyData)) { - let deprecatedText = this.tr("Services marked in yellow are deprecated, they will be retired soon."); - if (canIWrite) { - deprecatedText += " " + this.tr("They can be updated by pressing the Update button."); - } - const deprecatedLabel = new qx.ui.basic.Label(deprecatedText); - labels.push(deprecatedLabel); - } - if (this.self().anyServiceRetired(this._studyData)) { - let retiredText = this.tr("Services marked in red are retired: you cannot use them anymore."); - if (canIWrite) { - retiredText += "
" + this.tr("If the Update button is disabled, they might require manual intervention to be updated:"); - retiredText += "
- " + this.tr("Open the study"); - retiredText += "
- " + this.tr("Click on the retired service, download the data"); - retiredText += "
- " + this.tr("Upload the data to an updated version"); + const introText = new qx.ui.basic.Label().set({ + font: "text-14", + rich: true + }); + this._introText.add(introText); + let msg = ""; + const params = { + url: { + studyId: this._studyData["uuid"] } - const retiredLabel = new qx.ui.basic.Label(retiredText); - labels.push(retiredLabel); - } - const updatableServices = this.self().updatableNodeIds(this._studyData["workbench"]); - if (updatableServices.length === 0) { - const upToDateText = this.tr("All services are up to date to their latest compatible version."); - const upToDateLabel = new qx.ui.basic.Label(upToDateText); - labels.push(upToDateLabel); - } else if (canIWrite) { - const useUpdateButtonText = this.tr("Use the Update buttons to bring the services to their latest compatible version."); - const useUpdateButtonLabel = new qx.ui.basic.Label(useUpdateButtonText); - labels.push(useUpdateButtonLabel); - } else { - const notUpToDateText = this.tr("Some services are not up to date."); - const notUpToDateLabel = new qx.ui.basic.Label(notUpToDateText); - labels.push(notUpToDateLabel); - } + }; + osparc.data.Resources.fetch("studies", "getServices", params) + .then(resp => { + const services = resp["services"]; + if (osparc.study.Utils.getCantExecuteServices(services).length) { + msg += this.tr("Some services are not accessible. Please contact service owner:"); + msg += "

"; + } + if (osparc.study.Utils.anyServiceRetired(services)) { + msg += this.tr("Services marked in red are retired: you cannot use them anymore."); + if (canIWrite) { + msg += "
" + this.tr("If the Update button is disabled, they might require manual intervention to be updated:"); + msg += "
- " + this.tr("Open the study"); + msg += "
- " + this.tr("Click on the retired service, download the data"); + msg += "
- " + this.tr("Upload the data to an updated version"); + } + msg += "

"; + } + if (osparc.study.Utils.anyServiceDeprecated(services)) { + msg += this.tr("Services marked in yellow are deprecated, they will be retired soon."); + if (canIWrite) { + msg += " " + this.tr("They can be updated by pressing the Update button."); + } + msg += "

"; + } + const anyServiceUpdatable = osparc.study.Utils.anyServiceUpdatable(services); + if (anyServiceUpdatable === false && msg === "") { + msg += this.tr("All services are up to date to their latest compatible version."); + msg += "
"; + } else if (canIWrite) { + msg += this.tr("Use the Update buttons to bring the services to their latest compatible version."); + msg += "
"; + } else { + msg += this.tr("Some services are not up to date."); + } - labels.forEach(label => { - label.set({ - font: "text-14", - rich: true + introText.setValue(msg); }); - this._introText.add(label); - }); }, __updateService: async function(nodeId, key, version, button) { - const latestCompatible = osparc.service.Utils.getLatestCompatible(key, version); + const latestCompatible = osparc.store.Services.getLatestCompatible(key, version); const patchData = {}; if (key !== latestCompatible["key"]) { patchData["key"] = latestCompatible["key"]; @@ -201,6 +168,7 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { const canIWrite = osparc.data.model.Study.canIWrite(this._studyData["accessRights"]); let i = 0; + const updatableServices = []; const workbench = this._studyData["workbench"]; for (const nodeId in workbench) { i++; @@ -218,7 +186,7 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { const compatibleVersionLabel = new qx.ui.basic.Label().set({ font: "text-14" }); - const latestCompatible = osparc.service.Utils.getLatestCompatible(node["key"], node["version"]); + const latestCompatible = osparc.store.Services.getLatestCompatible(node["key"], node["version"]); if (latestCompatible) { // updatable osparc.store.Services.getService(latestCompatible["key"], latestCompatible["version"]) @@ -241,9 +209,9 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { column: this.self().GRID_POS.COMPATIBLE_VERSION }); - const isUpdatable = osparc.service.Utils.isUpdatable(node); if (latestCompatible && canIWrite) { const updateButton = new osparc.ui.form.FetchButton(null, "@MaterialIcons/update/14"); + const isUpdatable = osparc.service.Utils.isUpdatable(metadata); updateButton.set({ enabled: isUpdatable }); @@ -257,6 +225,7 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { label: this.tr("Update"), center: true }); + updatableServices.push(nodeId); } updateButton.addListener("execute", () => this.__updateService(nodeId, node["key"], node["version"], updateButton), this); this._servicesGrid.add(updateButton, { @@ -266,7 +235,6 @@ qx.Class.define("osparc.metadata.ServicesInStudyUpdate", { } } - const updatableServices = osparc.metadata.ServicesInStudyUpdate.updatableNodeIds(workbench); if (updatableServices.length && canIWrite) { const updateAllButton = this.__updateAllButton; updateAllButton.show(); diff --git a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js index 5f810b187991..ea6ed0fbb289 100644 --- a/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js +++ b/services/static-webserver/client/source/class/osparc/node/LifeCycleView.js @@ -110,7 +110,7 @@ qx.Class.define("osparc.node.LifeCycleView", { }); updateButton.addListener("execute", () => { updateButton.setFetching(true); - const latestCompatible = osparc.service.Utils.getLatestCompatible(node.getKey(), node.getVersion()); + const latestCompatible = osparc.store.Services.getLatestCompatible(node.getKey(), node.getVersion()); if (node.getKey() !== latestCompatible["key"]) { node.setKey(latestCompatible["key"]); } diff --git a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js index f339a68bf06a..9d9b4bfa656f 100644 --- a/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js +++ b/services/static-webserver/client/source/class/osparc/notification/NotificationUI.js @@ -150,7 +150,7 @@ qx.Class.define("osparc.notification.NotificationUI", { "studyId": resourceId } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(study => { const studyAlias = osparc.product.Utils.getStudyAlias({ firstUpperCase: true @@ -194,7 +194,7 @@ qx.Class.define("osparc.notification.NotificationUI", { "studyId": resourceId } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(study => titleLabel.setValue(`Note added in '${study["name"]}'`)) .catch(() => this.setEnabled(false)); } @@ -283,7 +283,7 @@ qx.Class.define("osparc.notification.NotificationUI", { "studyId": studyId } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(studyData => { if (studyData) { const studyDataCopy = osparc.data.model.Study.deepCloneStudyObject(studyData); diff --git a/services/static-webserver/client/source/class/osparc/service/ServiceListItem.js b/services/static-webserver/client/source/class/osparc/service/ServiceListItem.js index 9fbc302b6fbb..6c007907944c 100644 --- a/services/static-webserver/client/source/class/osparc/service/ServiceListItem.js +++ b/services/static-webserver/client/source/class/osparc/service/ServiceListItem.js @@ -24,8 +24,7 @@ qx.Class.define("osparc.service.ServiceListItem", { this.set({ width: this.self().ITEM_WIDTH, height: this.self().ITEM_HEIGHT, - paddingTop: 0, - paddingBottom: 0, + padding: this.self().PADDING, allowGrowX: true, focusable: true, }); @@ -53,7 +52,8 @@ qx.Class.define("osparc.service.ServiceListItem", { statics: { LATEST: "latest", ITEM_WIDTH: 550, - ITEM_HEIGHT: 35, + ITEM_HEIGHT: 32 + 2*4, // thumbnail + 2*PADDING + PADDING: 4, SERVICE_THUMBNAIL: osparc.product.Utils.getThumbnailUrl() }, diff --git a/services/static-webserver/client/source/class/osparc/service/Utils.js b/services/static-webserver/client/source/class/osparc/service/Utils.js index d58094d5900b..ab1ee841c74e 100644 --- a/services/static-webserver/client/source/class/osparc/service/Utils.js +++ b/services/static-webserver/client/source/class/osparc/service/Utils.js @@ -24,7 +24,7 @@ * Here is a little example of how to use the widget. * *
- *   let latestSrv = osparc.service.Utils.getLatest(key);
+ *   let latestSrv = osparc.store.Services.getLatest(key);
  * 
*/ @@ -130,86 +130,11 @@ qx.Class.define("osparc.service.Utils", { return services; }, - getVersions: function(key, filterDeprecated = true) { - const services = osparc.store.Services.servicesCached; - let versions = []; - if (key in services) { - const serviceVersions = services[key]; - versions = versions.concat(Object.keys(serviceVersions)); - if (filterDeprecated) { - versions = versions.filter(version => { - if (services[key][version]["retired"]) { - return false; - } - return true; - }); - } - versions.sort(osparc.utils.Utils.compareVersionNumbers); - } - return versions.reverse(); - }, - - getLatest: function(key) { - const services = osparc.store.Services.servicesCached; - if (key in services) { - const versions = this.getVersions(key, true); - if (versions.length) { - return services[key][versions[0]]; - } - } - return null; - }, - - getLatestCompatible: function(key, version) { - const services = osparc.store.Services.servicesCached; - if (key in services && version in services[key]) { - const serviceMD = services[key][version]; - if (serviceMD["compatibility"] && serviceMD["compatibility"]["canUpdateTo"]) { - const canUpdateTo = serviceMD["compatibility"]["canUpdateTo"]; - return { - key: "key" in canUpdateTo ? canUpdateTo["key"] : key, // key is optional - version: canUpdateTo["version"] - } - } - // the provided key/version itself is the latest compatible - return { - key, - version - } - } - return null; - }, - - getVersionDisplay: function(key, version) { - const services = osparc.store.Services.servicesCached; - if (key in services && version in services[key]) { - return this.extractVersionDisplay(services[key][version]); - } - return null; - }, - extractVersionDisplay: function(metadata) { - return metadata["versionDisplay"] ? metadata["versionDisplay"] : metadata["version"]; - }, - - getReleasedDate: function(key, version) { - const services = osparc.store.Services.servicesCached; - if ( - key in services && - version in services[key] && - "released" in services[key][version] - ) { - return services[key][version]["released"]; + if (metadata) { + return metadata["versionDisplay"] ? metadata["versionDisplay"] : metadata["version"]; } - return null; - }, - - versionToListItem: function(key, version) { - const versionDisplay = this.getVersionDisplay(key, version); - const listItem = new qx.ui.form.ListItem(versionDisplay); - osparc.utils.Utils.setIdToWidget(listItem, "serviceVersionItem_" + versionDisplay); - listItem.version = version; - return listItem; + return ""; }, canIWrite: function(serviceAccessRights) { @@ -228,14 +153,38 @@ qx.Class.define("osparc.service.Utils", { DEPRECATED_AUTOUPDATABLE_INSTRUCTIONS: qx.locale.Manager.tr("Please Stop the Service and then Update it"), RETIRED_AUTOUPDATABLE_INSTRUCTIONS: qx.locale.Manager.tr("Please Update the Service"), + extractVersionFromHistory: function(metadata) { + if (metadata["history"]) { + const found = metadata["history"].find(historyEntry => historyEntry["version"] === metadata["version"]); + return found; + } + return null; + }, + isUpdatable: function(metadata) { - const latestCompatible = this.getLatestCompatible(metadata["key"], metadata["version"]); - return latestCompatible && (metadata["key"] !== latestCompatible["key"] || metadata["version"] !== latestCompatible["version"]); + const historyEntry = this.extractVersionFromHistory(metadata); + if (historyEntry && historyEntry["compatibility"] && historyEntry["compatibility"]["canUpdateTo"]) { + const latestCompatible = historyEntry["compatibility"]["canUpdateTo"]; + return latestCompatible && (metadata["key"] !== latestCompatible["key"] || metadata["version"] !== latestCompatible["version"]); + } + return false; + }, + + __extractRetiredDate: function(metadata) { + if ("release" in metadata && metadata["release"]["retired"]) { + // this works for service latest + return new Date(metadata["release"]["retired"]); + } + const historyEntry = this.extractVersionFromHistory(metadata); + if (historyEntry && "retired" in historyEntry && historyEntry["retired"]) { + return new Date(historyEntry["retired"]); + } + return null; }, isDeprecated: function(metadata) { - if (metadata && "retired" in metadata && ![null, undefined].includes(metadata["retired"])) { - const deprecationTime = new Date(metadata["retired"]); + const deprecationTime = this.__extractRetiredDate(metadata); + if (deprecationTime) { const now = new Date(); return deprecationTime.getTime() > now.getTime(); } @@ -243,8 +192,8 @@ qx.Class.define("osparc.service.Utils", { }, isRetired: function(metadata) { - if (metadata && "retired" in metadata && ![null, undefined].includes(metadata["retired"])) { - const deprecationTime = new Date(metadata["retired"]); + const deprecationTime = this.__extractRetiredDate(metadata); + if (deprecationTime) { const now = new Date(); return deprecationTime.getTime() < now.getTime(); } @@ -252,34 +201,11 @@ qx.Class.define("osparc.service.Utils", { }, getDeprecationDateText: function(metadata) { - const deprecationTime = new Date(metadata["retired"]); - return qx.locale.Manager.tr("It will be Retired: ") + osparc.utils.Utils.formatDate(deprecationTime); - }, - - getFilePicker: function() { - return this.self().getLatest("simcore/services/frontend/file-picker"); - }, - - getParametersMetadata: function() { - const parametersMetadata = []; - const services = osparc.store.Services.servicesCached; - for (const key in services) { - if (key.includes("simcore/services/frontend/parameter/")) { - const latest = this.self().getLatest(key); - if (latest) { - parametersMetadata.push(latest); - } - } + if (this.isDeprecated(metadata) || this.isRetired(metadata)) { + const deprecationTime = this.__extractRetiredDate(metadata); + return qx.locale.Manager.tr("It will be Retired: ") + osparc.utils.Utils.formatDate(deprecationTime); } - return parametersMetadata; - }, - - getParameterMetadata: function(type) { - return this.self().getLatest("simcore/services/frontend/parameter/"+type); - }, - - getProbeMetadata: function(type) { - return this.self().getLatest("simcore/services/frontend/iterator-consumer/probe/"+type); + return ""; }, removeFileToKeyMap: function(service) { diff --git a/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js b/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js index c9b1557f1112..b80382fdd17a 100644 --- a/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js +++ b/services/static-webserver/client/source/class/osparc/snapshots/IterationsView.js @@ -76,7 +76,7 @@ qx.Class.define("osparc.snapshots.IterationsView", { "studyId": iteration["workcopy_project_id"] } }; - iterationPromises.push(osparc.data.Resources.getOne("studies", params)); + iterationPromises.push(osparc.data.Resources.fetch("studies", "getOne", params)); }); Promise.all(iterationPromises) .then(values => { @@ -204,7 +204,7 @@ qx.Class.define("osparc.snapshots.IterationsView", { "studyId": iterationId } }; - osparc.data.Resources.getOne("studies", params) + osparc.data.Resources.fetch("studies", "getOne", params) .then(data => { const studyData = this.__study.serialize(); studyData["workbench"] = data["workbench"]; diff --git a/services/static-webserver/client/source/class/osparc/store/Services.js b/services/static-webserver/client/source/class/osparc/store/Services.js index c2abeed32ece..ded5d21101e0 100644 --- a/services/static-webserver/client/source/class/osparc/store/Services.js +++ b/services/static-webserver/client/source/class/osparc/store/Services.js @@ -19,11 +19,11 @@ qx.Class.define("osparc.store.Services", { type: "static", statics: { - servicesCached: {}, + __servicesCached: {}, getServicesLatest: function(useCache = true) { return new Promise(resolve => { - if (useCache && Object.keys(this.servicesCached)) { + if (useCache && Object.keys(this.__servicesCached)) { // return latest only const latest = this.__getLatestCached(); resolve(latest); @@ -37,9 +37,8 @@ qx.Class.define("osparc.store.Services", { this.__addTSRInfos(servicesObj); this.__addExtraTypeInfos(servicesObj); - // use response to populate servicesCached Object.values(servicesObj).forEach(serviceKey => { - Object.values(serviceKey).forEach(srv => this.__addToCache(srv)); + Object.values(serviceKey).forEach(service => this.__addToCache(service)); }); resolve(servicesObj); @@ -52,6 +51,110 @@ qx.Class.define("osparc.store.Services", { }); }, + getLatest: function(key) { + const services = this.__servicesCached; + if (key in services) { + const latestMetadata = Object.values(services[key])[0]; + if (!osparc.service.Utils.isRetired(latestMetadata)) { + return latestMetadata; + } + } + return null; + }, + + getLatestCompatible: function(key, version) { + const services = this.__servicesCached; + if (key in services && version in services[key]) { + const serviceMD = services[key][version]; + if (serviceMD["compatibility"] && serviceMD["compatibility"]["canUpdateTo"]) { + const canUpdateTo = serviceMD["compatibility"]["canUpdateTo"]; + return { + key: "key" in canUpdateTo ? canUpdateTo["key"] : key, // key is optional + version: canUpdateTo["version"] + }; + } + // the provided key/version itself is the latest compatible + return { + key, + version + }; + } + return null; + }, + + getVersionDisplay: function(key, version) { + const services = this.__servicesCached; + if (key in services && version in services[key]) { + return osparc.service.Utils.extractVersionDisplay(services[key][version]); + } + return null; + }, + + getReleasedDate: function(key, version) { + const services = this.__servicesCached; + if ( + key in services && + version in services[key] && + "released" in services[key][version] + ) { + return services[key][version]["released"]; + } + return null; + }, + + getService: function(key, version, useCache = true) { + return new Promise(resolve => { + if ( + useCache && + this.__isInCache(key, version) && + "history" in this.__servicesCached[key][version] + ) { + resolve(this.__servicesCached[key][version]); + return; + } + + const params = { + url: osparc.data.Resources.getServiceUrl(key, version) + }; + osparc.data.Resources.fetch("servicesV2", "getOne", params) + .then(service => { + this.__addHit(service); + this.__addTSRInfo(service); + this.__addExtraTypeInfo(service); + this.__addToCache(service) + resolve(service); + }) + .catch(console.error); + }); + }, + + __getAllVersions: function(key) { + const services = this.__servicesCached; + let versions = []; + if (key in services) { + const serviceVersions = services[key]; + versions = versions.concat(Object.keys(serviceVersions)); + versions.sort(osparc.utils.Utils.compareVersionNumbers); + } + return versions.reverse(); + }, + + populateVersionsSelectBox: function(key, selectBox) { + const versions = this.__getAllVersions(key); + return this.getService(key, versions[0]) + .then(latestMetadata => { + latestMetadata["history"].forEach(historyEntry => { + if (!historyEntry["retired"]) { + const versionDisplay = osparc.service.Utils.extractVersionDisplay(historyEntry); + const listItem = new qx.ui.form.ListItem(versionDisplay); + osparc.utils.Utils.setIdToWidget(listItem, "serviceVersionItem_" + versionDisplay); + listItem.version = historyEntry["version"]; + selectBox.add(listItem); + } + }); + }); + }, + getServicesLatestList: function(excludeFrontend = true, excludeDeprecated = true) { return new Promise(resolve => { const servicesList = []; @@ -65,19 +168,25 @@ qx.Class.define("osparc.store.Services", { // do not add frontend services continue; } - if (excludeDeprecated && serviceLatest["retired"]) { - // first check if a previous version of this service isn't retired - let versions = Object.keys(this.servicesCached[key]); - versions = versions.sort(osparc.utils.Utils.compareVersionNumbers).reverse(); - for (let j=0; j { - if (useCache && this.__isInCache(key, version)) { - resolve(this.servicesCached[key][version]); - return; - } - - const params = { - url: osparc.data.Resources.getServiceUrl(key, version) - }; - osparc.data.Resources.getOne("servicesV2", params) - .then(service => { - this.__addHit(service); - this.__addTSRInfo(service); - this.__addExtraTypeInfo(service); - this.__addToCache(service) - resolve(service); - }) - .catch(console.error); - }); - }, - getResources: function(key, version) { return new Promise(resolve => { if ( this.__isInCache(key, version) && - "resources" in this.servicesCached[key][version] + "resources" in this.__servicesCached[key][version] ) { - resolve(this.servicesCached[key][version]["resources"]); + resolve(this.__servicesCached[key][version]["resources"]); return; } @@ -129,7 +216,7 @@ qx.Class.define("osparc.store.Services", { }; osparc.data.Resources.get("serviceResources", params) .then(resources => { - this.servicesCached[key][version]["resources"] = resources; + this.__servicesCached[key][version]["resources"] = resources; resolve(resources); }); }); @@ -137,7 +224,7 @@ qx.Class.define("osparc.store.Services", { getMetadata: function(key, version) { if (this.__isInCache(key, version)) { - return this.servicesCached[key][version]; + return this.__servicesCached[key][version]; } return null; }, @@ -153,50 +240,97 @@ qx.Class.define("osparc.store.Services", { }; return osparc.data.Resources.fetch("servicesV2", "patch", params) .then(() => { - this.servicesCached[key][version][fieldKey] = value; + this.__servicesCached[key][version][fieldKey] = value; serviceData[fieldKey] = value; }); }, + getStudyServicesMetadata: function(studyData) { + const wbServices = new Set(osparc.study.Utils.extractUniqueServices(studyData["workbench"])); + const promises = []; + wbServices.forEach(srv => { + promises.push(this.getService(srv["key"], srv["version"])); + }); + return Promise.all(promises); + }, + + getInaccessibleServices: function(workbench) { + const allServices = this.__servicesCached; + const unaccessibleServices = []; + const wbServices = new Set(osparc.study.Utils.extractUniqueServices(workbench)); + wbServices.forEach(srv => { + if (srv.key in allServices && srv.version in allServices[srv.key]) { + return; + } + const idx = unaccessibleServices.findIndex(unSrv => unSrv.key === srv.key && unSrv.version === srv.version); + if (idx === -1) { + unaccessibleServices.push(srv); + } + }); + return unaccessibleServices; + }, + + getInaccessibleServicesMsg: function(inaccessibleServices, workbench) { + let msg = qx.locale.Manager.tr("Some services are not accessible:
"); + Object.values(workbench).forEach(node => { + const inaccessibleService = inaccessibleServices.find(srv => srv.key === node.key && srv.version === node.version); + if (inaccessibleService) { + const n = inaccessibleService.key.lastIndexOf("/"); + const friendlyKey = inaccessibleService.key.substring(n + 1); + msg += `- ${node.label} (${friendlyKey}:${inaccessibleService.version})
`; + } + }); + return msg; + }, + + getFilePicker: function() { + return this.getLatest("simcore/services/frontend/file-picker"); + }, + + getParametersMetadata: function() { + const parametersMetadata = []; + const services = this.__servicesCached; + for (const key in services) { + if (key.includes("simcore/services/frontend/parameter/")) { + const latest = this.getLatest(key); + if (latest) { + parametersMetadata.push(latest); + } + } + } + return parametersMetadata; + }, + + getParameterMetadata: function(type) { + return this.getLatest("simcore/services/frontend/parameter/"+type); + }, + + getProbeMetadata: function(type) { + return this.getLatest("simcore/services/frontend/iterator-consumer/probe/"+type); + }, + __addToCache: function(service) { const key = service.key; const version = service.version; - if (!(key in this.servicesCached)) { - this.servicesCached[key] = {}; - } - this.servicesCached[key][version] = service; - this.servicesCached[key][version]["cached"] = true; - - if ("history" in service) { - service["history"].forEach(historyEntry => { - const hVersion = historyEntry.version; - if (!(hVersion in this.servicesCached[key])) { - this.servicesCached[key][hVersion] = {}; - this.servicesCached[key][hVersion]["cached"] = false; - } - // merge history data into current metadata - this.servicesCached[key][hVersion] = { - ...this.servicesCached[key][hVersion], - ...historyEntry - }; - }); + if (!(key in this.__servicesCached)) { + this.__servicesCached[key] = {}; } + this.__servicesCached[key][version] = service; }, __isInCache: function(key, version) { return ( - key in this.servicesCached && - version in this.servicesCached[key] && - this.servicesCached[key][version]["cached"] + key in this.__servicesCached && + version in this.__servicesCached[key] ); }, __getLatestCached: function() { const latestServices = {}; - for (const key in this.servicesCached) { - let versions = Object.keys(this.servicesCached[key]); + for (const key in this.__servicesCached) { + let versions = Object.keys(this.__servicesCached[key]); versions = versions.sort(osparc.utils.Utils.compareVersionNumbers).reverse(); - const latest = this.servicesCached[key][versions[0]]; + const latest = this.__servicesCached[key][versions[0]]; latestServices[key] = osparc.utils.Utils.deepCloneObject(latest); } return latestServices; 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 b4ea84778a69..a784cdd1335f 100644 --- a/services/static-webserver/client/source/class/osparc/study/StudyOptions.js +++ b/services/static-webserver/client/source/class/osparc/study/StudyOptions.js @@ -245,7 +245,7 @@ qx.Class.define("osparc.study.StudyOptions", { } }; Promise.all([ - osparc.data.Resources.getOne("studies", params), + osparc.data.Resources.fetch("studies", "getOne", params), osparc.data.Resources.fetch("studies", "getWallet", params) ]) .then(values => { diff --git a/services/static-webserver/client/source/class/osparc/study/Utils.js b/services/static-webserver/client/source/class/osparc/study/Utils.js index 7748d60303fd..52152cb1f60e 100644 --- a/services/static-webserver/client/source/class/osparc/study/Utils.js +++ b/services/static-webserver/client/source/class/osparc/study/Utils.js @@ -45,77 +45,45 @@ qx.Class.define("osparc.study.Utils", { return services; }, - getInaccessibleServices: function(workbench) { - const allServices = osparc.store.Services.servicesCached; - const unaccessibleServices = []; - const wbServices = new Set(this.extractUniqueServices(workbench)); - wbServices.forEach(srv => { - if (srv.key in allServices && srv.version in allServices[srv.key]) { - return; - } - const idx = unaccessibleServices.findIndex(unSrv => unSrv.key === srv.key && unSrv.version === srv.version); - if (idx === -1) { - unaccessibleServices.push(srv); - } - }); - return unaccessibleServices; + getCantExecuteServices: function(studyServices = []) { + return studyServices.filter(studyService => studyService["myAccessRights"]["execute"] === false); }, - getInaccessibleServicesMsg: function(inaccessibleServices, workbench) { - let msg = qx.locale.Manager.tr("Service(s) not accessible:
"); - Object.values(workbench).forEach(node => { - const inaccessibleService = inaccessibleServices.find(srv => srv.key === node.key && srv.version === node.version); - if (inaccessibleService) { - const n = inaccessibleService.key.lastIndexOf("/"); - const friendlyKey = inaccessibleService.key.substring(n + 1); - msg += `- ${node.label} (${friendlyKey}:${inaccessibleService.version})
`; + anyServiceRetired: function(studyServices) { + const isRetired = studyServices.some(studyService => { + if (studyService["release"] && studyService["release"]["retired"]) { + const retirementDate = new Date(studyService["release"]["retired"]); + const currentDate = new Date(); + return retirementDate < currentDate; } + return false; }); - return msg; - }, - - isWorkbenchUpdatable: function(workbench) { - const services = new Set(this.extractUniqueServices(workbench)); - const isUpdatable = Array.from(services).some(srv => osparc.service.Utils.isUpdatable(srv)); - return isUpdatable; + return isRetired; }, - isWorkbenchRetired: function(workbench) { - const allServices = osparc.store.Services.servicesCached; - const services = new Set(this.extractUniqueServices(workbench)); - const isRetired = Array.from(services).some(srv => { - if (srv.key in allServices && srv.version in allServices[srv.key]) { - const serviceMD = allServices[srv.key][srv.version]; - if (serviceMD["retired"]) { - const retirementDate = new Date(serviceMD["retired"]); - const currentDate = new Date(); - return retirementDate < currentDate; - } - return false; + anyServiceDeprecated: function(studyServices) { + const isDeprecated = studyServices.some(studyService => { + if (studyService["release"] && studyService["release"]["retired"]) { + const retirementDate = new Date(studyService["release"]["retired"]); + const currentDate = new Date(); + return retirementDate > currentDate; } return false; }); - return isRetired; + return isDeprecated; }, - isWorkbenchDeprecated: function(workbench) { - const allServices = osparc.store.Services.servicesCached; - const services = new Set(this.extractUniqueServices(workbench)); - const isRetired = Array.from(services).some(srv => { - if (srv.key in allServices && srv.version in allServices[srv.key]) { - const serviceMD = allServices[srv.key][srv.version]; - if ("retired" in serviceMD && serviceMD["retired"]) { - const retirementDate = new Date(serviceMD["retired"]); - const currentDate = new Date(); - return retirementDate > currentDate; - } - return false; + anyServiceUpdatable: function(studyServices) { + const isUpdatable = studyServices.some(studyService => { + if (studyService["release"] && studyService["release"]["compatibility"]) { + return Boolean(studyService["release"]["compatibility"]); } return false; }); - return isRetired; + return isUpdatable; }, + createStudyFromService: function(key, version, existingStudies, newStudyLabel, contextProps = {}) { return new Promise((resolve, reject) => { osparc.store.Services.getService(key, version) @@ -157,9 +125,9 @@ qx.Class.define("osparc.study.Utils", { if (!("mode" in minStudyData["ui"])) { minStudyData["ui"]["mode"] = "standalone"; } - const inaccessibleServices = this.getInaccessibleServices(minStudyData["workbench"]) + const inaccessibleServices = osparc.store.Services.getInaccessibleServices(minStudyData["workbench"]) if (inaccessibleServices.length) { - const msg = this.getInaccessibleServicesMsg(inaccessibleServices, minStudyData["workbench"]); + const msg = osparc.store.Services.getInaccessibleServicesMsg(inaccessibleServices, minStudyData["workbench"]); reject({ message: msg }); @@ -204,9 +172,9 @@ qx.Class.define("osparc.study.Utils", { createStudyFromTemplate: function(templateData, loadingPage, contextProps = {}) { return new Promise((resolve, reject) => { - const inaccessibleServices = this.getInaccessibleServices(templateData["workbench"]); + const inaccessibleServices = osparc.store.Services.getInaccessibleServices(templateData["workbench"]); if (inaccessibleServices.length) { - const msg = this.getInaccessibleServicesMsg(inaccessibleServices, templateData["workbench"]); + const msg = osparc.store.Services.getInaccessibleServicesMsg(inaccessibleServices, templateData["workbench"]); reject({ message: msg }); @@ -293,8 +261,8 @@ qx.Class.define("osparc.study.Utils", { }, __getBlockedState: function(studyData) { - if (studyData["workbench"]) { - const unaccessibleServices = osparc.study.Utils.getInaccessibleServices(studyData["workbench"]) + if (studyData["services"]) { + const unaccessibleServices = osparc.study.Utils.getCantExecuteServices(studyData["services"]) if (unaccessibleServices.length) { return "UNKNOWN_SERVICES"; } @@ -362,30 +330,34 @@ qx.Class.define("osparc.study.Utils", { return Object.values(studyData["workbench"]).filter(nodeData => !osparc.data.model.Node.isFrontend(nodeData)); }, - guessIcon: function(studyData) { + guessIcon: async function(studyData) { if (osparc.product.Utils.isProduct("tis") || osparc.product.Utils.isProduct("tiplite")) { return this.__guessTIPIcon(studyData); } - return this.__guessIcon(studyData); + const icon = await this.__guessIcon(studyData); + return icon; }, __guessIcon: function(studyData) { - // the was to guess the TI type is to check the boot mode of the ti-postpro in the pipeline - const wbServices = this.self().getNonFrontendNodes(studyData); - if (wbServices.length === 1) { - const wbService = wbServices[0]; - const allServices = osparc.store.Services.servicesCached; - if (wbService.key in allServices && wbService.version in allServices[wbService.key]) { - const serviceMetadata = allServices[wbService.key][wbService.version]; - if (serviceMetadata["icon"]) { - return serviceMetadata["icon"]; - } + const defaultIcon = osparc.dashboard.CardBase.PRODUCT_ICON; + return new Promise(resolve => { + // the was to guess the TI type is to check the boot mode of the ti-postpro in the pipeline + const wbServices = this.self().getNonFrontendNodes(studyData); + if (wbServices.length === 1) { + const wbService = wbServices[0]; + osparc.store.Services.getService(wbService.key, wbService.version) + .then(serviceMetadata => { + if (serviceMetadata["icon"]) { + resolve(serviceMetadata["icon"]); + } + resolve(defaultIcon); + }); + } else if (wbServices.length > 1) { + resolve("osparc/icons/diagram.png"); + } else { + resolve(defaultIcon); } - } - if (wbServices.length > 1) { - return "osparc/icons/diagram.png"; - } - return osparc.dashboard.CardBase.PRODUCT_ICON; + }); }, __guessTIPIcon: function(studyData) { diff --git a/services/static-webserver/client/source/class/osparc/utils/Utils.js b/services/static-webserver/client/source/class/osparc/utils/Utils.js index 885a09d4f0c2..781e0cba3d9f 100644 --- a/services/static-webserver/client/source/class/osparc/utils/Utils.js +++ b/services/static-webserver/client/source/class/osparc/utils/Utils.js @@ -91,11 +91,20 @@ qx.Class.define("osparc.utils.Utils", { FLOATING_Z_INDEX: 1000001 + 1, + checkImageExists: function(url) { + return new Promise(resolve => { + const img = new Image(); + img.onload = () => resolve(true); + img.onerror = () => resolve(false); + img.src = url; + }); + }, + setUrlSourceToImage: function(image, imgSrc) { let source = osparc.product.Utils.getThumbnailUrl(); - fetch(imgSrc, { method: "HEAD" }) - .then(response => { - if (response.ok) { + this.checkImageExists(imgSrc) + .then(exists => { + if (exists) { source = imgSrc; } }) diff --git a/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js b/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js index d14212aa20f5..94229ae175e0 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js +++ b/services/static-webserver/client/source/class/osparc/workbench/ServiceCatalog.js @@ -225,35 +225,30 @@ qx.Class.define("osparc.workbench.ServiceCatalog", { }); osparc.service.Utils.sortObjectsBasedOn(filteredServices, this.__sortBy); - const filteredServicesObj = this.__filteredServicesObj = osparc.service.Utils.convertArrayToObject(filteredServices); - - const groupedServicesList = []; - for (const key in filteredServicesObj) { - const serviceMetadata = osparc.service.Utils.getLatest(key); - if (serviceMetadata) { - const service = new osparc.data.model.Service(serviceMetadata); - groupedServicesList.push(service); - } - } + this.__filteredServicesObj = osparc.service.Utils.convertArrayToObject(filteredServices); + + const servicesModels = []; + filteredServices.forEach(filteredService => { + const service = new osparc.data.model.Service(filteredService); + servicesModels.push(service); + }); - this.__serviceList.setModel(new qx.data.Array(groupedServicesList)); + this.__serviceList.setModel(new qx.data.Array(servicesModels)); }, __changedSelection: function(key) { if (this.__versionsBox) { - let selectBox = this.__versionsBox; + const selectBox = this.__versionsBox; selectBox.removeAll(); if (key in this.__filteredServicesObj) { const latest = new qx.ui.form.ListItem(this.self().LATEST); latest.version = this.self().LATEST; selectBox.add(latest); - const versions = osparc.service.Utils.getVersions(key); - versions.forEach(version => { - const listItem = osparc.service.Utils.versionToListItem(key, version); - selectBox.add(listItem); - }); - osparc.utils.Utils.growSelectBox(selectBox, 200); - selectBox.setSelection([latest]); + osparc.store.Services.populateVersionsSelectBox(key, selectBox) + .then(() => { + osparc.utils.Utils.growSelectBox(selectBox, 200); + selectBox.setSelection([latest]); + }); } } if (this.__addBtn) { diff --git a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js index 19a123cf5473..221fc08417a5 100644 --- a/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js +++ b/services/static-webserver/client/source/class/osparc/workbench/WorkbenchUI.js @@ -2005,7 +2005,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { x: e.offsetX, y: e.offsetY }; - const service = qx.data.marshal.Json.createModel(osparc.service.Utils.getFilePicker()); + const service = qx.data.marshal.Json.createModel(osparc.store.Services.getFilePicker()); const nodeUI = await this.__addNode(service, pos); if (nodeUI) { const filePicker = new osparc.file.FilePicker(nodeUI.getNode(), "workbench"); @@ -2028,7 +2028,7 @@ qx.Class.define("osparc.workbench.WorkbenchUI", { const data = this.__isDraggingLink["dragData"]; this.__isDraggingLink = null; const pos = this.__pointerEventToWorkbenchPos(e, false); - const service = qx.data.marshal.Json.createModel(osparc.service.Utils.getFilePicker()); + const service = qx.data.marshal.Json.createModel(osparc.store.Services.getFilePicker()); const nodeUI = await this.__addNode(service, pos); if (nodeUI) { const node = nodeUI.getNode(); diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index cd57f9576b35..a2a2f0735c05 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -2027,7 +2027,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Page_CatalogServiceListItem_' + $ref: '#/components/schemas/Page_CatalogLatestServiceGet_' /v0/catalog/services/{service_key}/{service_version}: get: tags: @@ -7855,7 +7855,7 @@ components: - default - items title: BootOption - CatalogServiceGet: + CatalogLatestServiceGet: properties: key: type: string @@ -7914,11 +7914,9 @@ components: inputs: type: object title: Inputs - description: inputs with extended information outputs: type: object title: Outputs - description: outputs with extended information bootOptions: anyOf: - type: object @@ -7944,19 +7942,12 @@ components: type: array - type: 'null' title: Classifiers - default: [] quality: type: object title: Quality - default: {} - history: - items: - $ref: '#/components/schemas/ServiceRelease' - type: array - title: History - description: history of releases for this service at this point in time, - starting from the newest to the oldest. It includes current release. - default: [] + release: + $ref: '#/components/schemas/ServiceRelease' + description: release information of current (latest) service type: object required: - key @@ -7970,7 +7961,8 @@ components: - inputs - outputs - accessRights - title: CatalogServiceGet + - release + title: CatalogLatestServiceGet example: accessRights: '1': @@ -7984,61 +7976,105 @@ components: contact: contact@acme.com description: A service which awaits for time to pass, two times. description_ui: true - history: - - released: '2024-07-20T15:00:00' - version: 2.2.1 - version_display: Summer Release - - compatibility: - canUpdateTo: - version: 2.2.1 - version: 2.0.0 - - version: 0.9.11 - - version: 0.9.10 - - compatibility: - canUpdateTo: - version: 0.9.11 - version: 0.9.8 - - compatibility: - can_update_to: - version: 0.9.11 - released: '2024-01-20T18:49:17' - version: 0.9.1 - versionDisplay: Matterhorn - - retired: '2024-07-20T15:00:00' - version: 0.9.0 - - version: 0.8.0 - - version: 0.1.0 icon: https://cdn-icons-png.flaticon.com/512/25/25231.png inputs: - input0: - contentSchema: - title: Acceleration - type: number - x_unit: m/s**2 - description: acceleration with units + input_1: + description: Pick a file containing only one integer + displayOrder: 1 + fileToKeyMap: + single_number.txt: input_1 keyId: input_1 - label: Acceleration + label: File with int number + type: data:text/plain + input_2: + contentSchema: + minimum: 0 + title: Sleep interval + type: integer + x_unit: second + defaultValue: 2 + description: Choose an amount of time to sleep in range [0:] + displayOrder: 2 + keyId: input_2 + label: Sleep interval type: ref_contentSchema - unitLong: meter/second3 - unitShort: m/s3 + unitLong: second + unitShort: s + input_3: + defaultValue: false + description: If set to true will cause service to fail after it sleeps + displayOrder: 3 + keyId: input_3 + label: Fail after sleep + type: boolean + input_4: + contentSchema: + title: Distance to bed + type: integer + x_unit: meter + defaultValue: 0 + description: It will first walk the distance to bed + displayOrder: 4 + keyId: input_4 + label: Distance to bed + type: ref_contentSchema + unitLong: meter + unitShort: m + input_5: + contentSchema: + minimum: 0 + title: Dream of the night + type: integer + x_unit: byte + defaultValue: 0 + description: Defines the size of the dream that will be generated [0:] + displayOrder: 5 + keyId: input_5 + label: Dream (or nightmare) of the night + type: ref_contentSchema + unitLong: byte + unitShort: B key: simcore/services/comp/itis/sleeper name: sleeper outputs: - outFile: - description: Time the service waited before completion + output_1: + description: Integer is generated in range [1-9] + displayOrder: 1 + fileToKeyMap: + single_number.txt: output_1 + keyId: output_1 + label: File containing one random integer + type: data:text/plain + output_2: + contentSchema: + title: Random sleep interval + type: integer + x_unit: second + description: Interval is generated in range [1-9] displayOrder: 2 keyId: output_2 - label: Time Slept - type: number - unit: second - unitLong: seconds - unitShort: sec + label: Random sleep interval + type: ref_contentSchema + unitLong: second + unitShort: s + output_3: + description: Contains some random data representing a dream + displayOrder: 3 + fileToKeyMap: + dream.txt: output_3 + keyId: output_3 + label: Dream output + type: data:text/plain owner: owner@acme.com quality: {} + release: + released: '2025-07-20T15:00:00' + version: 2.2.1 + version_display: Summer Release type: computational version: 2.2.1 version_display: 2 Xtreme - CatalogServiceListItem: + CatalogServiceGet: properties: key: type: string @@ -8097,9 +8133,11 @@ components: inputs: type: object title: Inputs + description: inputs with extended information outputs: type: object title: Outputs + description: outputs with extended information bootOptions: anyOf: - type: object @@ -8125,20 +8163,17 @@ components: type: array - type: 'null' title: Classifiers - default: [] quality: type: object title: Quality - default: {} history: items: $ref: '#/components/schemas/ServiceRelease' type: array title: History - description: History will be replaced by current 'release' instead + description: history of releases for this service at this point in time, + starting from the newest to the oldest. It includes current release. default: [] - deprecated: true - additionalProperties: false type: object required: - key @@ -8152,7 +8187,74 @@ components: - inputs - outputs - accessRights - title: CatalogServiceListItem + title: CatalogServiceGet + example: + accessRights: + '1': + execute: true + write: false + authors: + - affiliation: ACME + email: author@acme.com + name: Author Bar + classifiers: [] + contact: contact@acme.com + description: A service which awaits for time to pass, two times. + description_ui: true + history: + - released: '2024-07-21T15:00:00' + version: 2.2.1 + version_display: Summer Release + - compatibility: + canUpdateTo: + version: 2.2.1 + version: 2.0.0 + - version: 0.9.11 + - version: 0.9.10 + - compatibility: + canUpdateTo: + version: 0.9.11 + version: 0.9.8 + - compatibility: + can_update_to: + version: 0.9.11 + released: '2024-01-20T18:49:17' + version: 0.9.1 + versionDisplay: Matterhorn + - retired: '2024-07-20T16:00:00' + version: 0.9.0 + - version: 0.8.0 + - version: 0.1.0 + icon: https://cdn-icons-png.flaticon.com/512/25/25231.png + inputs: + input0: + contentSchema: + title: Acceleration + type: number + x_unit: m/s**2 + description: acceleration with units + keyId: input_1 + label: Acceleration + type: ref_contentSchema + unitLong: meter/second3 + unitShort: m/s3 + key: simcore/services/comp/itis/sleeper + name: sleeper + outputs: + outFile: + description: Time the service waited before completion + displayOrder: 2 + keyId: output_2 + label: Time Slept + type: number + unit: second + unitLong: seconds + unitShort: sec + owner: owner@acme.com + quality: {} + type: computational + version: 2.2.1 + version_display: 2 Xtreme CatalogServiceUpdate: properties: name: @@ -12391,7 +12493,7 @@ components: - total - count title: PageMetaInfoLimitOffset - Page_CatalogServiceListItem_: + Page_CatalogLatestServiceGet_: properties: _meta: $ref: '#/components/schemas/PageMetaInfoLimitOffset' @@ -12399,7 +12501,7 @@ components: $ref: '#/components/schemas/PageLinks' data: items: - $ref: '#/components/schemas/CatalogServiceListItem' + $ref: '#/components/schemas/CatalogLatestServiceGet' type: array title: Data additionalProperties: false @@ -12408,7 +12510,7 @@ components: - _meta - _links - data - title: Page[CatalogServiceListItem] + title: Page[CatalogLatestServiceGet] Page_LicensedItemPurchaseGet_: properties: _meta: diff --git a/services/web/server/src/simcore_service_webserver/catalog/_controller_rest.py b/services/web/server/src/simcore_service_webserver/catalog/_controller_rest.py index b4d14c41991e..644edb14d9ae 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_controller_rest.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_controller_rest.py @@ -12,6 +12,7 @@ from aiohttp import web from aiohttp.web import Request, RouteTableDef from models_library.api_schemas_webserver.catalog import ( + CatalogLatestServiceGet, CatalogServiceGet, CatalogServiceUpdate, ) @@ -83,7 +84,7 @@ async def list_services_latest(request: Request): assert page_meta.limit == query_params.limit # nosec assert page_meta.offset == query_params.offset # nosec - page = Page[CatalogServiceGet].model_validate( + page = Page[CatalogLatestServiceGet].model_validate( paginate_data( chunk=page_items, request_url=request.url, diff --git a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py index 171537099216..e005192edaea 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py +++ b/services/web/server/tests/unit/with_dbs/01/test_catalog_handlers__services.py @@ -11,7 +11,7 @@ from aiohttp.test_utils import TestClient from aioresponses import aioresponses as AioResponsesMock from faker import Faker -from models_library.api_schemas_catalog.services import ServiceGetV2 +from models_library.api_schemas_catalog.services import LatestServiceGet, ServiceGetV2 from models_library.api_schemas_webserver.catalog import ( CatalogServiceGet, CatalogServiceUpdate, @@ -63,12 +63,12 @@ async def _list( assert product_name assert user_id - items = TypeAdapter(list[ServiceGetV2]).validate_python( - ServiceGetV2.model_json_schema()["examples"], + items = TypeAdapter(list[LatestServiceGet]).validate_python( + LatestServiceGet.model_json_schema()["examples"], ) total_count = len(items) - return PageRpc[ServiceGetV2].create( + return PageRpc[LatestServiceGet].create( items[offset : offset + limit], total=total_count, limit=limit,