diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/3810966d1534_adding_pricing_and_harware_info_to_comp_.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/3810966d1534_adding_pricing_and_harware_info_to_comp_.py new file mode 100644 index 00000000000..b15524ce9de --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/3810966d1534_adding_pricing_and_harware_info_to_comp_.py @@ -0,0 +1,40 @@ +"""adding pricing and harware info to comp_tasks + +Revision ID: 3810966d1534 +Revises: 5c62b190e124 +Create Date: 2023-10-17 14:35:21.032940+00:00 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "3810966d1534" +down_revision = "5c62b190e124" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "comp_tasks", + sa.Column( + "pricing_info", postgresql.JSONB(astext_type=sa.Text()), nullable=True + ), + ) + op.add_column( + "comp_tasks", + sa.Column( + "hardware_info", postgresql.JSONB(astext_type=sa.Text()), nullable=True + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("comp_tasks", "hardware_info") + op.drop_column("comp_tasks", "pricing_info") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/comp_tasks.py b/packages/postgres-database/src/simcore_postgres_database/models/comp_tasks.py index a6602455111..60bfc3f95c3 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/comp_tasks.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/comp_tasks.py @@ -87,6 +87,19 @@ class NodeClass(enum.Enum): ), column_created_datetime(timezone=True), column_modified_datetime(timezone=True), + sa.Column( + "pricing_info", + postgresql.JSONB, + nullable=True, + doc="Billing information of this task", + ), + sa.Column( + "hardware_info", + postgresql.JSONB, + nullable=True, + doc="Harware information of this task", + ), + # ------ sa.UniqueConstraint("project_id", "node_id", name="project_node_uniqueness"), ) diff --git a/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py b/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py index 448c7be5e60..b1b9a6022f0 100644 --- a/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py +++ b/packages/postgres-database/src/simcore_postgres_database/utils_projects_nodes.py @@ -5,6 +5,10 @@ import sqlalchemy from aiopg.sa.connection import SAConnection +from simcore_postgres_database.models.projects_node_to_pricing_unit import ( + projects_node_to_pricing_unit, +) +from sqlalchemy.dialects.postgresql import insert as pg_insert from .errors import ForeignKeyViolation, UniqueViolation from .models.projects_nodes import projects_nodes @@ -193,3 +197,67 @@ async def delete(self, connection: SAConnection, *, node_id: uuid.UUID) -> None: & (projects_nodes.c.node_id == f"{node_id}") ) await connection.execute(delete_stmt) + + async def get_project_node_pricing_unit_id( + self, connection: SAConnection, *, node_uuid: uuid.UUID + ) -> tuple | None: + """get a pricing unit that is connected to the project node or None if there is non connected + + NOTE: Do not use this in an asyncio.gather call as this will fail! + """ + result = await connection.execute( + sqlalchemy.select( + projects_node_to_pricing_unit.c.pricing_plan_id, + projects_node_to_pricing_unit.c.pricing_unit_id, + ) + .select_from( + projects_nodes.join( + projects_node_to_pricing_unit, + projects_nodes.c.project_node_id + == projects_node_to_pricing_unit.c.project_node_id, + ) + ) + .where( + (projects_nodes.c.project_uuid == f"{self.project_uuid}") + & (projects_nodes.c.node_id == f"{node_uuid}") + ) + ) + row = await result.fetchone() + if row: + return (row[0], row[1]) + return None + + async def connect_pricing_unit_to_project_node( + self, + connection: SAConnection, + *, + node_uuid: uuid.UUID, + pricing_plan_id: int, + pricing_unit_id: int, + ) -> None: + result = await connection.scalar( + sqlalchemy.select(projects_nodes.c.project_node_id).where( + (projects_nodes.c.project_uuid == f"{self.project_uuid}") + & (projects_nodes.c.node_id == f"{node_uuid}") + ) + ) + project_node_id = int(result) if result else 0 + + insert_stmt = pg_insert(projects_node_to_pricing_unit).values( + project_node_id=project_node_id, + pricing_plan_id=pricing_plan_id, + pricing_unit_id=pricing_unit_id, + created=sqlalchemy.func.now(), + modified=sqlalchemy.func.now(), + ) + on_update_stmt = insert_stmt.on_conflict_do_update( + index_elements=[ + projects_node_to_pricing_unit.c.project_node_id, + ], + set_={ + "pricing_plan_id": insert_stmt.excluded.pricing_plan_id, + "pricing_unit_id": insert_stmt.excluded.pricing_unit_id, + "modified": sqlalchemy.func.now(), + }, + ) + await connection.execute(on_update_stmt) diff --git a/services/director-v2/src/simcore_service_director_v2/api/dependencies/rut_client.py b/services/director-v2/src/simcore_service_director_v2/api/dependencies/rut_client.py new file mode 100644 index 00000000000..70bc94b7ad2 --- /dev/null +++ b/services/director-v2/src/simcore_service_director_v2/api/dependencies/rut_client.py @@ -0,0 +1,7 @@ +from fastapi import Request + +from ...modules.resource_usage_tracker_client import ResourceUsageTrackerClient + + +def get_rut_client(request: Request) -> ResourceUsageTrackerClient: + return ResourceUsageTrackerClient.get_from_state(request.app) diff --git a/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py b/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py index ffe2b0cf395..bf4251ff9e6 100644 --- a/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py +++ b/services/director-v2/src/simcore_service_director_v2/api/routes/computations.py @@ -30,7 +30,7 @@ from models_library.clusters import DEFAULT_CLUSTER_ID from models_library.projects import ProjectAtDB, ProjectID from models_library.projects_nodes_io import NodeID -from models_library.services import ServiceKey, ServiceKeyVersion, ServiceVersion +from models_library.services import ServiceKeyVersion from models_library.users import UserID from models_library.utils.fastapi_encoders import jsonable_encoder from pydantic import AnyHttpUrl, parse_obj_as @@ -47,6 +47,7 @@ ClusterAccessForbiddenError, ClusterNotFoundError, ComputationalRunNotFoundError, + PricingPlanUnitNotFoundError, ProjectNotFoundError, SchedulerError, ) @@ -62,7 +63,7 @@ from ...modules.db.repositories.projects import ProjectsRepository from ...modules.db.repositories.users import UsersRepository from ...modules.director_v0 import DirectorV0Client -from ...modules.resource_usage_client import ResourceUsageApi +from ...modules.resource_usage_tracker_client import ResourceUsageTrackerClient from ...utils.computations import ( find_deprecated_tasks, get_pipeline_state_from_task_states, @@ -82,6 +83,7 @@ from ..dependencies.catalog import get_catalog_client from ..dependencies.database import get_repository from ..dependencies.director_v0 import get_director_v0_client +from ..dependencies.rut_client import get_rut_client from ..dependencies.scheduler import get_scheduler from .computations_tasks import analyze_pipeline @@ -122,6 +124,7 @@ async def create_computation( # noqa: C901, PLR0912 scheduler: Annotated[BaseCompScheduler, Depends(get_scheduler)], catalog_client: Annotated[CatalogClient, Depends(get_catalog_client)], users_repo: Annotated[UsersRepository, Depends(get_repository(UsersRepository))], + rut_client: Annotated[ResourceUsageTrackerClient, Depends(get_rut_client)], ) -> ComputationGet: log.debug( "User %s is creating a new computation from project %s", @@ -205,6 +208,8 @@ async def create_computation( # noqa: C901, PLR0912 published_nodes=min_computation_nodes if computation.start_pipeline else [], user_id=computation.user_id, product_name=computation.product_name, + rut_client=rut_client, + is_wallet=bool(computation.wallet_info), ) if computation.start_pipeline: @@ -225,25 +230,10 @@ async def create_computation( # noqa: C901, PLR0912 # Billing info wallet_id = None wallet_name = None - pricing_plan_id = None - pricing_unit_id = None - pricing_unit_cost_id = None if computation.wallet_info: wallet_id = computation.wallet_info.wallet_id wallet_name = computation.wallet_info.wallet_name - resource_usage_api = ResourceUsageApi.get_from_state(request.app) - # NOTE: MD/SAN -> add real service version/key and store in DB, issue: https://github.com/ITISFoundation/osparc-issues/issues/1131 - ( - pricing_plan_id, - pricing_unit_id, - pricing_unit_cost_id, - ) = await resource_usage_api.get_default_service_pricing_plan_and_pricing_unit( - computation.product_name, - ServiceKey("simcore/services/comp/itis/sleeper"), - ServiceVersion("2.1.6"), - ) - await scheduler.run_new_pipeline( computation.user_id, computation.project_id, @@ -259,9 +249,6 @@ async def create_computation( # noqa: C901, PLR0912 user_email=await users_repo.get_user_email(computation.user_id), wallet_id=wallet_id, wallet_name=wallet_name, - pricing_plan_id=pricing_plan_id, - pricing_unit_id=pricing_unit_id, - pricing_unit_cost_id=pricing_unit_cost_id, ), use_on_demand_clusters=computation.use_on_demand_clusters, ) @@ -317,6 +304,8 @@ async def create_computation( # noqa: C901, PLR0912 raise HTTPException( status_code=status.HTTP_406_NOT_ACCEPTABLE, detail=f"{e}" ) from e + except PricingPlanUnitNotFoundError as e: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"{e}") from e @router.get( diff --git a/services/director-v2/src/simcore_service_director_v2/core/application.py b/services/director-v2/src/simcore_service_director_v2/core/application.py index eb3813206bd..9ad93646615 100644 --- a/services/director-v2/src/simcore_service_director_v2/core/application.py +++ b/services/director-v2/src/simcore_service_director_v2/core/application.py @@ -28,7 +28,7 @@ osparc_variables_substitutions, rabbitmq, remote_debug, - resource_usage_client, + resource_usage_tracker_client, storage, ) from .errors import ( @@ -176,7 +176,7 @@ def init_app(settings: AppSettings | None = None) -> FastAPI: comp_scheduler.setup(app) if settings.DIRECTOR_V2_RESOURCE_USAGE_TRACKER: - resource_usage_client.setup(app) + resource_usage_tracker_client.setup(app) node_rights.setup(app) diff --git a/services/director-v2/src/simcore_service_director_v2/core/errors.py b/services/director-v2/src/simcore_service_director_v2/core/errors.py index 0554e06d6c2..8cb99519416 100644 --- a/services/director-v2/src/simcore_service_director_v2/core/errors.py +++ b/services/director-v2/src/simcore_service_director_v2/core/errors.py @@ -92,6 +92,13 @@ def __init__(self, project_id: ProjectID): super().__init__(f"project {project_id} not found") +class PricingPlanUnitNotFoundError(DirectorException): + """Pricing plan unit not found error""" + + def __init__(self, msg: str): + super().__init__(msg) + + class PipelineNotFoundError(DirectorException): """Pipeline not found error""" diff --git a/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py b/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py index e6f54e66df4..03265d56744 100644 --- a/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py +++ b/services/director-v2/src/simcore_service_director_v2/models/comp_runs.py @@ -21,9 +21,6 @@ class RunMetadataDict(TypedDict, total=False): user_email: str wallet_id: int | None wallet_name: str | None - pricing_plan_id: int | None - pricing_unit_id: int | None - pricing_unit_cost_id: int | None class CompRunsAtDB(BaseModel): diff --git a/services/director-v2/src/simcore_service_director_v2/models/comp_tasks.py b/services/director-v2/src/simcore_service_director_v2/models/comp_tasks.py index abaa009e12f..1e6eef80701 100644 --- a/services/director-v2/src/simcore_service_director_v2/models/comp_tasks.py +++ b/services/director-v2/src/simcore_service_director_v2/models/comp_tasks.py @@ -138,6 +138,9 @@ class CompTaskAtDB(BaseModel): ) created: datetime.datetime modified: datetime.datetime + # Additional information about price and hardware (ex. AWS EC2 instance type) + pricing_info: dict | None + hardware_info: dict | None @validator("state", pre=True) @classmethod @@ -214,6 +217,12 @@ class Config: "last_heartbeat": None, "created": "2022-05-20 13:28:31.139+00", "modified": "2023-06-23 15:58:32.833081+00", + "pricing_info": { + "pricing_plan_id": 1, + "pricing_unit_id": 1, + "pricing_unit_cost_id": 1, + }, + "hardware_info": {"aws_ec2_instance": ["aws-specific-instance"]}, } for image_example in Image.Config.schema_extra["examples"] ] diff --git a/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py b/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py index effa63cd6b4..1f9776499ad 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/comp_scheduler/base_scheduler.py @@ -380,9 +380,15 @@ async def _process_started_tasks( ), wallet_id=run_metadata.get("wallet_id"), wallet_name=run_metadata.get("wallet_name"), - pricing_plan_id=run_metadata.get("pricing_plan_id"), - pricing_unit_id=run_metadata.get("pricing_unit_id"), - pricing_unit_cost_id=run_metadata.get("pricing_unit_cost_id"), + pricing_plan_id=t.pricing_info.get("pricing_plan_id") + if t.pricing_info + else None, + pricing_unit_id=t.pricing_info.get("pricing_unit_id") + if t.pricing_info + else None, + pricing_unit_cost_id=t.pricing_info.get("pricing_unit_cost_id") + if t.pricing_info + else None, product_name=run_metadata.get( "product_name", UNDEFINED_STR_METADATA ), diff --git a/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks.py b/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks.py index 9d44f4dcc3f..1612ec0f816 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/db/repositories/comp_tasks.py @@ -17,6 +17,7 @@ from models_library.projects_nodes import Node from models_library.projects_nodes_io import NodeID from models_library.projects_state import RunningState +from models_library.resource_tracker import HardwareInfo, PricingInfo from models_library.service_settings_labels import ( SimcoreServiceLabels, SimcoreServiceSettingsLabel, @@ -29,6 +30,9 @@ from servicelib.utils import logged_gather from simcore_postgres_database.utils_projects_nodes import ProjectNodesRepo from simcore_service_director_v2.core.errors import ComputationalTaskNotFoundError +from simcore_service_director_v2.modules.resource_usage_tracker_client import ( + ResourceUsageTrackerClient, +) from simcore_service_director_v2.utils.comp_scheduler import COMPLETED_STATES from sqlalchemy import literal_column from sqlalchemy.dialects.postgresql import insert @@ -134,18 +138,17 @@ async def _generate_task_image( catalog_client: CatalogClient, connection: aiopg.sa.connection.SAConnection, user_id: UserID, - project_uuid: ProjectID, node_id: NodeID, node: Node, node_extras: ServiceExtras | None, node_labels: SimcoreServiceLabels | None, + project_nodes_repo: ProjectNodesRepo, ) -> Image: # aggregates node_details and node_extras into Image data: dict[str, Any] = { "name": node.key, "tag": node.version, } - project_nodes_repo = ProjectNodesRepo(project_uuid=project_uuid) project_node = await project_nodes_repo.get(connection, node_id=node_id) node_resources = parse_obj_as(ServiceResourcesDict, project_node.required_resources) if not node_resources: @@ -171,6 +174,8 @@ async def _generate_tasks_list_from_project( user_id: UserID, product_name: str, connection: aiopg.sa.connection.SAConnection, + rut_client: ResourceUsageTrackerClient, + is_wallet: bool, ) -> list[CompTaskAtDB]: list_comp_tasks = [] @@ -205,15 +210,16 @@ async def _generate_tasks_list_from_project( if not node_details: continue + project_nodes_repo = ProjectNodesRepo(project_uuid=project.uuid) image = await _generate_task_image( catalog_client=catalog_client, connection=connection, user_id=user_id, - project_uuid=project.uuid, node_id=NodeID(node_id), node=node, node_extras=node_extras, node_labels=node_labels, + project_nodes_repo=project_nodes_repo, ) assert node.state is not None # nosec @@ -227,6 +233,44 @@ async def _generate_tasks_list_from_project( ): task_state = RunningState.PUBLISHED + pricing_info = None + hardware_info = None + if is_wallet: + output = await project_nodes_repo.get_project_node_pricing_unit_id( + connection, node_uuid=NodeID(node_id) + ) + if output: + pricing_plan_id, pricing_unit_id = output + pricing_unit_get = await rut_client.get_pricing_unit( + product_name, pricing_plan_id, pricing_unit_id + ) + pricing_unit_cost_id = pricing_unit_get.current_cost_per_unit_id + aws_ec2_instances = pricing_unit_get.specific_info["aws_ec2_instances"] + else: + ( + pricing_plan_id, + pricing_unit_id, + pricing_unit_cost_id, + aws_ec2_instances, + ) = await rut_client.get_default_pricing_and_hardware_info( + product_name, + node.key, + node.version, + ) + await project_nodes_repo.connect_pricing_unit_to_project_node( + connection, + node_uuid=NodeID(node_id), + pricing_plan_id=pricing_plan_id, + pricing_unit_id=pricing_unit_id, + ) + + pricing_info = PricingInfo( + pricing_plan_id=pricing_plan_id, + pricing_unit_id=pricing_unit_id, + pricing_unit_cost_id=pricing_unit_cost_id, + ) + hardware_info = HardwareInfo(aws_ec2_instances=aws_ec2_instances) + task_db = CompTaskAtDB( project_id=project.uuid, node_id=NodeID(node_id), @@ -246,6 +290,8 @@ async def _generate_tasks_list_from_project( last_heartbeat=None, created=arrow.utcnow().datetime, modified=arrow.utcnow().datetime, + pricing_info=pricing_info.dict() if pricing_info else None, + hardware_info=hardware_info.dict() if hardware_info else None, ) list_comp_tasks.append(task_db) @@ -314,6 +360,8 @@ async def upsert_tasks_from_project( published_nodes: list[NodeID], user_id: UserID, product_name: str, + rut_client: ResourceUsageTrackerClient, + is_wallet: bool, ) -> list[CompTaskAtDB]: # NOTE: really do an upsert here because of issue https://github.com/ITISFoundation/osparc-simcore/issues/2125 async with self.db_engine.acquire() as conn: @@ -327,6 +375,8 @@ async def upsert_tasks_from_project( user_id, product_name, conn, + rut_client, + is_wallet, ) # get current tasks result = await conn.execute( diff --git a/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py b/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_tracker_client.py similarity index 58% rename from services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py rename to services/director-v2/src/simcore_service_director_v2/modules/resource_usage_tracker_client.py index a7177ec610d..87edee57fc2 100644 --- a/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_client.py +++ b/services/director-v2/src/simcore_service_director_v2/modules/resource_usage_tracker_client.py @@ -8,17 +8,23 @@ import httpx from fastapi import FastAPI +from models_library.api_schemas_resource_usage_tracker.credit_transactions import ( + WalletTotalCredits, +) from models_library.api_schemas_resource_usage_tracker.pricing_plans import ( + PricingUnitGet, ServicePricingPlanGet, ) from models_library.products import ProductName from models_library.resource_tracker import ( + PricingAndHardwareInfoTuple, PricingPlanId, - PricingUnitCostId, PricingUnitId, ) from models_library.services import ServiceKey, ServiceVersion +from models_library.wallets import WalletID from pydantic import parse_obj_as +from simcore_service_director_v2.core.errors import PricingPlanUnitNotFoundError from ..core.settings import AppSettings @@ -26,12 +32,12 @@ @dataclass -class ResourceUsageApi: +class ResourceUsageTrackerClient: client: httpx.AsyncClient exit_stack: contextlib.AsyncExitStack @classmethod - def create(cls, settings: AppSettings) -> "ResourceUsageApi": + def create(cls, settings: AppSettings) -> "ResourceUsageTrackerClient": client = httpx.AsyncClient( base_url=settings.DIRECTOR_V2_RESOURCE_USAGE_TRACKER.api_base_url, ) @@ -84,34 +90,63 @@ async def get_default_service_pricing_plan( response.raise_for_status() return parse_obj_as(ServicePricingPlanGet, response.json()) - async def get_default_service_pricing_plan_and_pricing_unit( + async def get_default_pricing_and_hardware_info( self, product_name: ProductName, service_key: ServiceKey, service_version: ServiceVersion, - ) -> tuple[PricingPlanId, PricingUnitId, PricingUnitCostId]: - pricing_plan = await self.get_default_service_pricing_plan( - product_name, service_key, service_version + ) -> PricingAndHardwareInfoTuple: + service_pricing_plan_get = await self.get_default_service_pricing_plan( + product_name=product_name, + service_key=service_key, + service_version=service_version, ) - if pricing_plan: - default_pricing_plan = pricing_plan - default_pricing_unit = pricing_plan.pricing_units[0] - return ( - default_pricing_plan.pricing_plan_id, - default_pricing_unit.pricing_unit_id, - default_pricing_unit.current_cost_per_unit_id, - ) - raise ValueError( - f"No default pricing plan provided for requested service key: {service_key} version: {service_version} product: {product_name}" + for unit in service_pricing_plan_get.pricing_units: + if unit.default: + return PricingAndHardwareInfoTuple( + service_pricing_plan_get.pricing_plan_id, + unit.pricing_unit_id, + unit.current_cost_per_unit_id, + unit.specific_info["aws_ec2_instances"], + ) + raise PricingPlanUnitNotFoundError( + "Default pricing plan and unit does not exist" + ) + + async def get_pricing_unit( + self, + product_name: ProductName, + pricing_plan_id: PricingPlanId, + pricing_unit_id: PricingUnitId, + ) -> PricingUnitGet: + response = await self.client.get( + f"/pricing-plans/{pricing_plan_id}/pricing-units/{pricing_unit_id}", + params={ + "product_name": product_name, + }, + ) + response.raise_for_status() + return parse_obj_as(PricingUnitGet, response.json()) + + async def get_wallet_credits( + self, + product_name: ProductName, + wallet_id: WalletID, + ) -> WalletTotalCredits: + response = await self.client.post( + "/credit-transactions/credits:sum", + params={"product_name": product_name, "wallet_id": wallet_id}, ) + response.raise_for_status() + return parse_obj_as(WalletTotalCredits, response.json()) # # app # @classmethod - def get_from_state(cls, app: FastAPI) -> "ResourceUsageApi": - return cast("ResourceUsageApi", app.state.resource_usage_api) + def get_from_state(cls, app: FastAPI) -> "ResourceUsageTrackerClient": + return cast("ResourceUsageTrackerClient", app.state.resource_usage_api) @classmethod def setup(cls, app: FastAPI): @@ -139,4 +174,4 @@ async def on_shutdown(): def setup(app: FastAPI): assert app.state # nosec - ResourceUsageApi.setup(app) + ResourceUsageTrackerClient.setup(app) diff --git a/services/static-webserver/client/source/class/osparc/About.js b/services/static-webserver/client/source/class/osparc/About.js index 91ba0d9d8e7..33adf8a1984 100644 --- a/services/static-webserver/client/source/class/osparc/About.js +++ b/services/static-webserver/client/source/class/osparc/About.js @@ -28,7 +28,6 @@ qx.Class.define("osparc.About", { contentPadding: this.self().PADDING, showMaximize: false, showMinimize: false, - resizable: false, centerOnAppear: true, clickAwayClose: true, modal: true diff --git a/services/static-webserver/client/source/class/osparc/auth/ui/RegistrationView.js b/services/static-webserver/client/source/class/osparc/auth/ui/RegistrationView.js index 3aec74b16a8..e1cb079a2a7 100644 --- a/services/static-webserver/client/source/class/osparc/auth/ui/RegistrationView.js +++ b/services/static-webserver/client/source/class/osparc/auth/ui/RegistrationView.js @@ -94,7 +94,7 @@ qx.Class.define("osparc.auth.ui.RegistrationView", { // buttons const grp = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); - const submitBtn = this.__submitBtn = new qx.ui.form.Button(this.tr("Submit")).set({ + const submitBtn = this.__submitBtn = new osparc.ui.form.FetchButton(this.tr("Submit")).set({ center: true, appearance: "strong-button" }); @@ -118,7 +118,7 @@ qx.Class.define("osparc.auth.ui.RegistrationView", { password: password1.getValue(), confirm: password2.getValue(), invitation: invitationToken ? invitationToken : "" - }); + }, submitBtn); } } }, this); @@ -128,7 +128,8 @@ qx.Class.define("osparc.auth.ui.RegistrationView", { this.add(grp); }, - __submit: function(userData) { + __submit: function(userData, submitButton) { + submitButton.setFetching(true); osparc.auth.Manager.getInstance().register(userData) .then(log => { this.fireDataEvent("done", log.message); @@ -137,7 +138,8 @@ qx.Class.define("osparc.auth.ui.RegistrationView", { .catch(err => { const msg = err.message || this.tr("Cannot register user"); osparc.FlashMessenger.getInstance().logAs(msg, "ERROR"); - }); + }) + .finally(() => submitButton.setFetching(false)); }, _onAppear: function() { diff --git a/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js b/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js index 74764e6f455..18f680615dd 100644 --- a/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js +++ b/services/static-webserver/client/source/class/osparc/dashboard/ResourceMoreOptions.js @@ -104,28 +104,26 @@ qx.Class.define("osparc.dashboard.ResourceMoreOptions", { __servicesUpdatePage: null, __addToolbar: function() { - const toolbar = this.__toolbar = new qx.ui.container.Composite(new qx.ui.layout.HBox(40)); + const toolbar = this.__toolbar = new qx.ui.container.Composite(new qx.ui.layout.HBox(20)); const resourceData = this.__resourceData; const title = new qx.ui.basic.Label(resourceData.name).set({ font: "text-16", alignY: "middle", - maxWidth: this.self().WIDTH-100, + allowGrowX: true, rich: true, wrap: true }); - toolbar.add(title); + toolbar.add(title, { + flex: 1 + }); if (osparc.utils.Resources.isService(resourceData)) { const serviceVersionSelector = this.__createServiceVersionSelector(); toolbar.add(serviceVersionSelector); } - toolbar.add(new qx.ui.core.Spacer(), { - flex: 1 - }); - const openButton = new qx.ui.form.Button(this.tr("Open")).set({ appearance: "strong-button", font: "text-14", 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 43fd7aada03..6e0dbaff536 100644 --- a/services/static-webserver/client/source/class/osparc/data/Resources.js +++ b/services/static-webserver/client/source/class/osparc/data/Resources.js @@ -693,6 +693,15 @@ qx.Class.define("osparc.data.Resources", { } } }, + "productMetadata": { + useCache: true, + endpoints: { + get: { + method: "GET", + url: statics.API + "/products/{productName}" + } + } + }, "invitations": { endpoints: { post: { @@ -747,7 +756,7 @@ qx.Class.define("osparc.data.Resources", { /* * AUTO RECHARGE */ - "auto-recharge": { + "autoRecharge": { useCache: false, endpoints: { get: { @@ -1064,11 +1073,16 @@ qx.Class.define("osparc.data.Resources", { let message = null; let status = null; if (e.getData().error) { - const logs = e.getData().error.logs || null; + const errorData = e.getData().error; + const logs = errorData.logs || null; if (logs && logs.length) { message = logs[0].message; } - status = e.getData().error.status; + const errors = errorData.errors || []; + if (message === null && errors && errors.length) { + message = errors[0].message; + } + status = errorData.status; } else { const req = e.getRequest(); message = req.getResponse(); diff --git a/services/static-webserver/client/source/class/osparc/data/model/Wallet.js b/services/static-webserver/client/source/class/osparc/data/model/Wallet.js index 5ce8ca1dff7..c5e5ddc143f 100644 --- a/services/static-webserver/client/source/class/osparc/data/model/Wallet.js +++ b/services/static-webserver/client/source/class/osparc/data/model/Wallet.js @@ -96,7 +96,8 @@ qx.Class.define("osparc.data.model.Wallet", { autoRecharge: { check: "Object", init: null, - nullable: true + nullable: true, + event: "changeAutoRecharge" }, preferredWallet: { 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 c26e42a4739..db4631d89d9 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js +++ b/services/static-webserver/client/source/class/osparc/desktop/WorkbenchView.js @@ -607,20 +607,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { const nodeId = data.nodeId; const msg = data.msg; const logLevel = ("level" in data) ? data["level"] : "INFO"; - switch (logLevel) { - case "DEBUG": - this.__loggerView.debug(nodeId, msg); - break; - case "WARNING": - this.__loggerView.warn(nodeId, msg); - break; - case "ERROR": - this.__loggerView.error(nodeId, msg); - break; - default: - this.__loggerView.info(nodeId, msg); - break; - } + this.__logsToLogger(nodeId, [msg], logLevel); }, this); workbench.addListener("fileRequested", () => { @@ -633,6 +620,29 @@ qx.Class.define("osparc.desktop.WorkbenchView", { this.__workbenchUIConnected = true; }, + __logsToLogger: function(nodeId, logs, logLevel) { + // the node logger is mainly used in App Mode + const nodeLogger = this.__getNodeLogger(nodeId); + switch (logLevel) { + case "DEBUG": + this.__loggerView.debugs(nodeId, logs); + nodeLogger.debugs(nodeId, logs); + break; + case "WARNING": + this.__loggerView.warns(nodeId, logs); + nodeLogger.warns(nodeId, logs); + break; + case "ERROR": + this.__loggerView.errors(nodeId, logs); + nodeLogger.errors(nodeId, logs); + break; + default: + this.__loggerView.infos(nodeId, logs); + nodeLogger.infos(nodeId, logs); + break; + } + }, + __attachSocketEventHandlers: function() { // Listen to socket const socket = osparc.wrapper.WebSocket.getInstance(); @@ -650,24 +660,7 @@ qx.Class.define("osparc.desktop.WorkbenchView", { const messages = data["messages"]; const logLevelMap = osparc.widget.logger.LoggerView.LOG_LEVEL_MAP; const logLevel = ("log_level" in data) ? logLevelMap[data["log_level"]] : "INFO"; - switch (logLevel) { - case "DEBUG": - this.__loggerView.debugs(nodeId, messages); - break; - case "WARNING": - this.__loggerView.warns(nodeId, messages); - break; - case "ERROR": - this.__loggerView.errors(nodeId, messages); - break; - default: - this.__loggerView.infos(nodeId, messages); - break; - } - const nodeLogger = this.__getNodeLogger(nodeId); - if (nodeLogger) { - nodeLogger.infos(nodeId, messages); - } + this.__logsToLogger(nodeId, messages, logLevel); }, this); } socket.emit(slotName); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Activity.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Activity.js new file mode 100644 index 00000000000..c84dd469100 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Activity.js @@ -0,0 +1,218 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.Activity", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(15)); + + this.getChildControl("activity-intro"); + + const walletSelectorLayout = this.getChildControl("wallet-selector-layout"); + const walletSelector = walletSelectorLayout.getChildren()[1]; + + const loadingImage = this.getChildControl("loading-image"); + loadingImage.show(); + const table = this.getChildControl("activity-table"); + table.exclude(); + + this.__fetchData(); + walletSelector.addListener("changeSelection", () => { + this.__prevUsageRequestParams = null; + this.__nextUsageRequestParams = null; + this.__fetchData(); + }); + }, + + statics: { + ITEMS_PER_PAGE: 15 + }, + + members: { + __prevUsageRequestParams: null, + __nextUsageRequestParams: null, + + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "activity-intro": + control = new qx.ui.basic.Label().set({ + value: this.tr("Transactions and Usage both together. Go to their specific sections to get more details"), + font: "text-14" + }); + this._add(control); + break; + case "wallet-selector-layout": + control = osparc.desktop.credits.Utils.createWalletSelectorLayout("read"); + this._add(control); + break; + case "loading-image": + control = new qx.ui.basic.Image().set({ + source: "@FontAwesome5Solid/circle-notch/64", + alignX: "center", + alignY: "middle" + }); + control.getContentElement().addClass("rotate"); + this._add(control); + break; + case "activity-table": + control = new osparc.desktop.credits.ActivityTable().set({ + height: (this.self().ITEMS_PER_PAGE*20 + 40) + }); + this._add(control); + break; + case "page-buttons": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)).set({ + allowGrowX: true, + alignX: "center", + alignY: "middle" + }); + this._add(control); + break; + case "prev-page-button": { + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/chevron-left/12", + allowGrowX: false + }); + control.addListener("execute", () => this.__fetchData(this.__getPrevRequest())); + const pageButtons = this.getChildControl("page-buttons"); + pageButtons.add(control); + break; + } + case "current-page-label": { + control = new qx.ui.basic.Label().set({ + font: "text-14", + textAlign: "center", + alignY: "middle" + }); + const pageButtons = this.getChildControl("page-buttons"); + pageButtons.add(control); + break; + } + case "next-page-button": { + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/chevron-right/12", + allowGrowX: false + }); + control.addListener("execute", () => this.__fetchData(this.__getNextUsageRequest())); + const pageButtons = this.getChildControl("page-buttons"); + pageButtons.add(control); + break; + } + } + return control || this.base(arguments, id); + }, + + __fetchData: function(request) { + const loadingImage = this.getChildControl("loading-image"); + loadingImage.show(); + const table = this.getChildControl("activity-table"); + table.exclude(); + + if (request === undefined) { + request = this.__getNextUsageRequest(); + } + Promise.all([ + request, + osparc.data.Resources.fetch("payments", "get") + ]) + .then(responses => { + const usagesResp = responses[0]; + const usages = usagesResp["data"]; + const transactions = responses[1]["data"]; + const activities1 = osparc.desktop.credits.ActivityTable.usagesToActivities(usages); + // Filter out some transactions + const walletId = this.__getSelectedWalletId(); + const filteredTransactions = transactions.filter(transaction => transaction["completedStatus"] !== "FAILED" && transaction["walletId"] === walletId); + const activities2 = osparc.desktop.credits.ActivityTable.transactionsToActivities(filteredTransactions); + const activities = activities1.concat(activities2); + activities.sort((a, b) => new Date(b["date"]).getTime() - new Date(a["date"]).getTime()); + this.__setData(activities); + + this.__prevUsageRequestParams = usagesResp["_links"]["prev"]; + this.__nextUsageRequestParams = usagesResp["_links"]["next"]; + this.__evaluatePageButtons(usagesResp); + }) + .finally(() => { + loadingImage.exclude(); + table.show(); + }); + }, + + __getPrevRequest: function() { + const params = { + url: { + offset: this.self().ITEMS_PER_PAGE, + limit: this.self().ITEMS_PER_PAGE + } + }; + if (this.__prevUsageRequestParams) { + params.url.offset = osparc.utils.Utils.getParamFromURL(this.__prevUsageRequestParams, "offset"); + params.url.limit = osparc.utils.Utils.getParamFromURL(this.__prevUsageRequestParams, "limit"); + } + return this.__getUsageCommonRequest(params); + }, + + __getNextUsageRequest: function() { + const params = { + url: { + offset: 0, + limit: this.self().ITEMS_PER_PAGE + } + }; + if (this.__nextUsageRequestParams) { + params.url.offset = osparc.utils.Utils.getParamFromURL(this.__nextUsageRequestParams, "offset"); + params.url.limit = osparc.utils.Utils.getParamFromURL(this.__nextUsageRequestParams, "limit"); + } + return this.__getUsageCommonRequest(params); + }, + + __getSelectedWalletId: function() { + const walletSelector = this.getChildControl("wallet-selector-layout").getChildren()[1]; + const walletSelection = walletSelector.getSelection(); + return walletSelection && walletSelection.length ? walletSelection[0].walletId : null; + }, + + __getUsageCommonRequest: function(params) { + const options = { + resolveWResponse: true + }; + + const walletId = this.__getSelectedWalletId(); + if (walletId) { + params.url["walletId"] = walletId.toString(); + return osparc.data.Resources.fetch("resourceUsagePerWallet", "getPage", params, undefined, options); + } + return null; + }, + + __setData: function(data) { + const table = this.getChildControl("activity-table"); + table.addData(data); + }, + + __evaluatePageButtons:function(resp) { + // this is not correct because we are populating the table with two different resources + this.getChildControl("prev-page-button").setEnabled(Boolean(this.__prevUsageRequestParams)); + this.getChildControl("current-page-label").setValue(((resp["_meta"]["offset"]/this.self().ITEMS_PER_PAGE)+1).toString()); + this.getChildControl("next-page-button").setEnabled(Boolean(this.__nextUsageRequestParams)); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/ActivityTable.js b/services/static-webserver/client/source/class/osparc/desktop/credits/ActivityTable.js new file mode 100644 index 00000000000..7152c2fed45 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/ActivityTable.js @@ -0,0 +1,142 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +/** + * Both, usage and transactions, mixed + */ + +qx.Class.define("osparc.desktop.credits.ActivityTable", { + extend: osparc.ui.table.Table, + + construct: function() { + const model = new qx.ui.table.model.Simple(); + const cols = this.self().COLUMNS; + const colNames = Object.values(cols).map(col => col.title); + model.setColumns(colNames); + + this.base(arguments, model, { + tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj), + statusBarVisible: false + }); + this.makeItLoose(); + + const columnModel = this.getTableColumnModel(); + columnModel.getBehavior().setWidth(this.self().COLUMNS.wallet.pos, 100); + columnModel.getBehavior().setWidth(this.self().COLUMNS.invoice.pos, 60); + + if (!osparc.desktop.credits.Utils.areWalletsEnabled()) { + columnModel.setColumnVisible(this.self().COLUMNS.wallet.pos, false); + columnModel.setColumnVisible(this.self().COLUMNS.invoice.pos, false); + } + }, + + statics: { + COLUMNS: { + date: { + pos: 0, + title: qx.locale.Manager.tr("Date") + }, + type: { + pos: 1, + title: qx.locale.Manager.tr("Type") + }, + title: { + pos: 2, + title: qx.locale.Manager.tr("Title") + }, + credits: { + pos: 3, + title: qx.locale.Manager.tr("Credits") + }, + wallet: { + pos: 4, + title: qx.locale.Manager.tr("Credit Account") + }, + invoice: { + pos: 5, + title: qx.locale.Manager.tr("Invoice") + } + }, + + createPdfIconWithLink: function(link) { + return `Invoice`; + }, + + usagesToActivities: function(usages) { + const activities = []; + usages.forEach(usage => { + const activity = { + date: usage["started_at"], + type: "Usage", + title: usage["project_name"], + credits: usage["credit_cost"], + walletId: usage["wallet_id"], + invoice: "" + }; + activities.push(activity); + }); + return activities; + }, + + transactionsToActivities: function(transactions) { + const activities = []; + transactions.forEach(transaction => { + const activity = { + date: transaction["createdAt"], + type: "Transaction", + title: transaction["comment"] ? transaction["comment"] : "", + credits: transaction["osparcCredits"].toFixed(2), + walletId: transaction["walletId"], + invoice: transaction["invoice"] + }; + activities.push(activity); + }); + return activities; + }, + + respDataToTableRow: function(data) { + const cols = this.COLUMNS; + const newData = []; + newData[cols["date"].pos] = osparc.utils.Utils.formatDateAndTime(new Date(data["date"])); + newData[cols["type"].pos] = data["type"]; + newData[cols["credits"].pos] = data["credits"] ? data["credits"] : "-"; + const found = osparc.desktop.credits.Utils.getWallet(data["walletId"]); + newData[cols["wallet"].pos] = found ? found.getName() : data["walletId"]; + const invoiceUrl = data["invoice"]; + newData[cols["invoice"].pos] = invoiceUrl? this.createPdfIconWithLink(invoiceUrl) : ""; + return newData; + }, + + respDataToTableData: function(datas) { + const newDatas = []; + if (datas) { + for (const data of datas) { + const newData = this.respDataToTableRow(data); + newDatas.push(newData); + } + } + return newDatas; + } + }, + + members: { + addData: function(datas) { + const newDatas = this.self().respDataToTableData(datas); + this.setData(newDatas); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/AutoRecharge.js b/services/static-webserver/client/source/class/osparc/desktop/credits/AutoRecharge.js index 11dfa09958e..0d706631997 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/AutoRecharge.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/AutoRecharge.js @@ -126,7 +126,7 @@ qx.Class.define("osparc.desktop.credits.AutoRecharge", { walletId: wallet.getWalletId() } }; - osparc.data.Resources.fetch("auto-recharge", "get", params) + osparc.data.Resources.fetch("autoRecharge", "get", params) .then(arData => this.__populateForm(arData)) .catch(err => console.error(err.message)); }, @@ -223,6 +223,25 @@ qx.Class.define("osparc.desktop.credits.AutoRecharge", { }; }, + __updateAutoRecharge: function(enabled, fetchButton, successfulMsg) { + const wallet = this.getWallet(); + fetchButton.setFetching(true); + const params = { + url: { + walletId: wallet.getWalletId() + }, + data: this.__getFieldsData() + }; + params.data["enabled"] = enabled; + osparc.data.Resources.fetch("autoRecharge", "put", params) + .then(arData => { + this.__populateForm(arData); + wallet.setAutoRecharge(arData); + osparc.FlashMessenger.getInstance().logAs(successfulMsg, "INFO"); + }) + .finally(() => fetchButton.setFetching(false)); + }, + __getEnableAutoRechargeButton: function() { const enableAutoRechargeBtn = new osparc.ui.form.FetchButton().set({ label: this.tr("Enable"), @@ -231,23 +250,8 @@ qx.Class.define("osparc.desktop.credits.AutoRecharge", { maxWidth: 200, center: true }); - enableAutoRechargeBtn.addListener("execute", () => { - enableAutoRechargeBtn.setFetching(true); - const params = { - url: { - walletId: this.getWallet().getWalletId() - }, - data: this.__getFieldsData() - }; - params.data["enabled"] = true; - osparc.data.Resources.fetch("auto-recharge", "put", params) - .then(arData => { - this.__populateForm(arData); - const msg = this.tr("Auto recharge was successfully enabled"); - osparc.FlashMessenger.getInstance().logAs(msg, "INFO"); - }) - .finally(() => enableAutoRechargeBtn.setFetching(false)); - }); + const successfulMsg = this.tr("Auto recharge was successfully enabled"); + enableAutoRechargeBtn.addListener("execute", () => this.__updateAutoRecharge(true, enableAutoRechargeBtn, successfulMsg)); return enableAutoRechargeBtn; }, @@ -259,23 +263,8 @@ qx.Class.define("osparc.desktop.credits.AutoRecharge", { maxWidth: 200, center: true }); - saveAutoRechargeBtn.addListener("execute", () => { - saveAutoRechargeBtn.setFetching(true); - const params = { - url: { - walletId: this.getWallet().getWalletId() - }, - data: this.__getFieldsData() - }; - params.data["enabled"] = true; - osparc.data.Resources.fetch("auto-recharge", "put", params) - .then(arData => { - this.__populateForm(arData); - const msg = this.tr("Changes on the Auto recharge were successfully saved"); - osparc.FlashMessenger.getInstance().logAs(msg, "INFO"); - }) - .finally(() => saveAutoRechargeBtn.setFetching(false)); - }); + const successfulMsg = this.tr("Changes on the Auto recharge were successfully saved"); + saveAutoRechargeBtn.addListener("execute", () => this.__updateAutoRecharge(true, saveAutoRechargeBtn, successfulMsg)); return saveAutoRechargeBtn; }, @@ -287,23 +276,8 @@ qx.Class.define("osparc.desktop.credits.AutoRecharge", { maxWidth: 200, center: true }); - disableAutoRechargeBtn.addListener("execute", () => { - disableAutoRechargeBtn.setFetching(true); - const params = { - url: { - walletId: this.getWallet().getWalletId() - }, - data: this.__getFieldsData() - }; - params.data["enabled"] = false; - osparc.data.Resources.fetch("auto-recharge", "put", params) - .then(arData => { - this.__populateForm(arData); - const msg = this.tr("Auto recharge was successfully disabled"); - osparc.FlashMessenger.getInstance().logAs(msg, "INFO"); - }) - .finally(() => disableAutoRechargeBtn.setFetching(false)); - }); + const successfulMsg = this.tr("Auto recharge was successfully disabled"); + disableAutoRechargeBtn.addListener("execute", () => this.__updateAutoRecharge(false, disableAutoRechargeBtn, successfulMsg)); return disableAutoRechargeBtn; } } diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCredits.js b/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCredits.js index fcd3b40c827..fb2bff4fd56 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCredits.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/BuyCredits.js @@ -21,12 +21,34 @@ qx.Class.define("osparc.desktop.credits.BuyCredits", { construct: function() { this.base(arguments); - const grid = new qx.ui.layout.Grid(80, 50); - grid.setColumnMaxWidth(0, 400); - grid.setColumnMaxWidth(1, 400); - this._setLayout(grid); + this._setLayout(new qx.ui.layout.VBox(15)); - this.__buildLayout(); + this.getChildControl("credits-intro"); + + const walletSelectorLayout = this.getChildControl("wallet-selector-layout"); + const walletSelector = walletSelectorLayout.getChildren()[1]; + const walletSelection = walletSelector.getSelection(); + const selectedWalletId = walletSelection && walletSelection.length ? walletSelection[0].walletId : null; + const walletFound = osparc.desktop.credits.Utils.getWallet(selectedWalletId); + if (walletFound) { + this.setWallet(walletFound); + } + + this.getChildControl("credits-left-view"); + + this.__populateLayout(); + + const wallets = osparc.store.Store.getInstance().getWallets(); + walletSelector.addListener("changeSelection", e => { + const selection = e.getData(); + const walletId = selection[0].walletId; + const found = wallets.find(wallet => wallet.getWalletId() === parseInt(walletId)); + if (found) { + this.setWallet(found); + } else { + this.setWallet(null); + } + }); }, properties: { @@ -47,106 +69,102 @@ qx.Class.define("osparc.desktop.credits.BuyCredits", { _createChildControlImpl: function(id) { let control; switch (id) { - case "wallet-layout": - control = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); - this._add(control, { - row: 0, - column: 0 + case "credits-intro": + control = this.__getCreditsExplanation(); + this._add(control); + break; + case "wallet-selector-layout": + control = osparc.desktop.credits.Utils.createWalletSelectorLayout("read"); + this._add(control); + break; + case "credits-left-view": + control = this.__getCreditsLeftView(); + this._add(control); + break; + case "wallet-billing-settings": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(30)); + this._add(control); + break; + case "payment-mode-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.HBox(5)); + this.getChildControl("wallet-billing-settings").add(control); + break; + case "payment-mode-title": + control = new qx.ui.basic.Label(this.tr("Payment mode")).set({ + font: "text-14" }); + this.getChildControl("payment-mode-layout").add(control); break; - case "explanation-layout": - control = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); - this._add(control, { - row: 0, - column: 1 + case "payment-mode": { + this.getChildControl("payment-mode-title"); + control = new qx.ui.form.SelectBox().set({ + allowGrowX: false, + allowGrowY: false }); + const autoItem = new qx.ui.form.ListItem(this.tr("Automatic"), null, "automatic"); + control.add(autoItem); + const manualItem = new qx.ui.form.ListItem(this.tr("Manual"), null, "manual"); + control.add(manualItem); + this.getChildControl("payment-mode-layout").add(control); break; + } case "one-time-payment": - control = new osparc.desktop.credits.OneTimePayment(); + control = new osparc.desktop.credits.OneTimePayment().set({ + maxWidth: 300 + }); this.bind("wallet", control, "wallet"); control.addListener("transactionCompleted", () => this.fireEvent("transactionCompleted")); - this._add(control, { - row: 1, - column: 0 - }); + this.getChildControl("wallet-billing-settings").add(control); break; case "auto-recharge": - control = new osparc.desktop.credits.AutoRecharge(); - this.bind("wallet", control, "wallet"); - this._add(control, { - row: 1, - column: 1 + control = new osparc.desktop.credits.AutoRecharge().set({ + maxWidth: 300 }); - break; - case "wallet-info": { - control = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)); - const label = new qx.ui.basic.Label().set({ - value: this.tr("Credit Account:"), - font: "text-14" - }); - control.add(label); - this.getChildControl("wallet-layout").add(control); - break; - } - case "wallet-selector": - control = this.__getWalletSelector(); - this.getChildControl("wallet-info").add(control); - break; - case "credits-left-view": - control = this.__getCreditsLeftView(); - this.getChildControl("wallet-info").add(control); - break; - case "credits-explanation": - control = this.__getCreditsExplanation(); - this.getChildControl("explanation-layout").add(control); + this.bind("wallet", control, "wallet"); + this.getChildControl("wallet-billing-settings").add(control); break; } return control || this.base(arguments, id); }, - __applyWallet: function(wallet) { + __populateLayout: function() { + const wallet = this.getWallet(); + console.log("wallet", wallet); if (wallet) { - const walletSelector = this.getChildControl("wallet-selector"); - walletSelector.getSelectables().forEach(selectable => { - if (selectable.walletId === wallet.getWalletId()) { - walletSelector.setSelection([selectable]); + const paymentMode = this.getChildControl("payment-mode"); + const autoRecharge = this.getChildControl("auto-recharge"); + const oneTime = this.getChildControl("one-time-payment"); + autoRecharge.show(); + oneTime.exclude(); + paymentMode.addListener("changeSelection", e => { + const model = e.getData()[0].getModel(); + if (model === "manual") { + autoRecharge.exclude(); + oneTime.show(); + } else { + autoRecharge.show(); + oneTime.exclude(); } }); } }, - __buildLayout: function() { - this.getChildControl("wallet-selector"); - this.getChildControl("credits-left-view"); - this.getChildControl("one-time-payment"); - this.getChildControl("credits-explanation"); - this.getChildControl("one-time-payment"); - this.getChildControl("auto-recharge"); - }, - - __getWalletSelector: function() { - const walletSelector = osparc.desktop.credits.Utils.createWalletSelector("write", false, false); - - walletSelector.addListener("changeSelection", e => { - const selection = e.getData(); - if (selection.length) { - const store = osparc.store.Store.getInstance(); - const found = store.getWallets().find(wallet => wallet.getWalletId() === parseInt(selection[0].walletId)); - if (found) { - this.setWallet(found); + __applyWallet: function(wallet) { + if (wallet) { + const walletSelectorLayout = this.getChildControl("wallet-selector-layout"); + const walletSelector = walletSelectorLayout.getChildren()[1]; + walletSelector.getSelectables().forEach(selectable => { + if (selectable.walletId === wallet.getWalletId()) { + walletSelector.setSelection([selectable]); } - } - }); - - if (walletSelector.getSelectables().length) { - walletSelector.setSelection([walletSelector.getSelectables()[0]]); + }); } - - return walletSelector; }, __getCreditsLeftView: function() { - const creditsIndicator = new osparc.desktop.credits.CreditsIndicator(); + const creditsIndicator = new osparc.desktop.credits.CreditsIndicator().set({ + maxWidth: 200 + }); creditsIndicator.getChildControl("credits-label").set({ alignX: "left" }); @@ -157,21 +175,13 @@ qx.Class.define("osparc.desktop.credits.BuyCredits", { __getCreditsExplanation: function() { const layout = new qx.ui.container.Composite(new qx.ui.layout.VBox(20)); - const label1 = new qx.ui.basic.Label().set({ + const label = new qx.ui.basic.Label().set({ value: "Explain here what a Credit is and what one can run/do with them.", - font: "text-16", - rich: true, - wrap: true - }); - layout.add(label1); - - const label2 = new qx.ui.basic.Label().set({ - value: "If something goes wrong you won't be charged", - font: "text-16", + font: "text-14", rich: true, wrap: true }); - layout.add(label2); + layout.add(label); return layout; } diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/OneTimePayment.js b/services/static-webserver/client/source/class/osparc/desktop/credits/OneTimePayment.js index 7625e38bdb2..6a50644c7ed 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/OneTimePayment.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/OneTimePayment.js @@ -307,10 +307,7 @@ qx.Class.define("osparc.desktop.credits.OneTimePayment", { const modal = true; const useNativeModalDialog = false; // this allow using the Blocker - const blocker = qx.bom.Window.getBlocker(); - blocker.setBlockerColor("#FFF"); - blocker.setBlockerOpacity(0.6); - let pgWindow = qx.bom.Window.open( + const pgWindow = osparc.desktop.credits.PaymentGatewayWindow.popUp( url, "pgWindow", options, @@ -318,27 +315,6 @@ qx.Class.define("osparc.desktop.credits.OneTimePayment", { useNativeModalDialog ); - // enhance the blocker - const blockerDomEl = blocker.getBlockerElement(); - blockerDomEl.style.cursor = "pointer"; - - // text on blocker - const label = document.createElement("h1"); - label.innerHTML = "Don’t see the secure Payment Window?
Click here to complete your purchase"; - label.style.position = "fixed"; - const labelWidth = 550; - const labelHeight = 100; - label.style.width = labelWidth + "px"; - label.style.height = labelHeight + "px"; - const root = qx.core.Init.getApplication().getRoot(); - if (root && root.getBounds()) { - label.style.left = Math.round(root.getBounds().width/2) - labelWidth/2 + "px"; - label.style.top = Math.round(root.getBounds().height/2) - labelHeight/2 + "px"; - } - blockerDomEl.appendChild(label); - - blockerDomEl.addEventListener("click", () => pgWindow.focus()); - // Listen to socket event const socket = osparc.wrapper.WebSocket.getInstance(); const slotName = "paymentCompleted"; diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Overview.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Overview.js deleted file mode 100644 index be0cd8cfbd0..00000000000 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/Overview.js +++ /dev/null @@ -1,346 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2023 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -qx.Class.define("osparc.desktop.credits.Overview", { - extend: qx.ui.core.Widget, - - construct: function() { - this.base(arguments); - - const grid = new qx.ui.layout.Grid(20, 20); - grid.setColumnFlex(0, 1); - grid.setColumnFlex(1, 1); - this._setLayout(grid); - - this.__buildLayout(); - }, - - events: { - "buyCredits": "qx.event.type.Data", - "toWallets": "qx.event.type.Event", - "toTransactions": "qx.event.type.Event", - "toUsageOverview": "qx.event.type.Event" - }, - - members: { - _createChildControlImpl: function(id) { - let control; - switch (id) { - case "wallets-card": { - const content = this.__createWalletsView(); - const wallets = osparc.store.Store.getInstance().getWallets(); - control = this.__createOverviewCard(`Credit Accounts (${wallets.length})`, content, "toWallets"); - control.getChildren()[0].setValue(this.tr("Credits")); - this._add(control, { - column: 0, - row: 0 - }); - break; - } - case "transactions-card": { - const content = this.__createTransactionsView(); - control = this.__createOverviewCard("Transactions", content, "toTransactions"); - this._add(control, { - column: 1, - row: 0 - }); - break; - } - case "usage-card": { - const content = this.__createUsageView(); - control = this.__createOverviewCard("Usage", content, "toUsageOverview"); - this._add(control, { - column: 0, - row: 1, - colSpan: 2 - }); - break; - } - } - return control || this.base(arguments, id); - }, - - __buildLayout: function() { - this.getChildControl("wallets-card"); - this.getChildControl("transactions-card"); - this.getChildControl("usage-card"); - }, - - __createOverviewCard: function(cardName, content, signalName) { - const layout = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)).set({ - minWidth: 200, - minHeight: 200, - padding: 15, - backgroundColor: "background-main-1" - }); - layout.getContentElement().setStyles({ - "border-radius": "4px" - }); - - const title = new qx.ui.basic.Label().set({ - value: cardName, - font: "text-14" - }); - layout.add(title); - - content.setPadding(5); - layout.add(content, { - flex: 1 - }); - - const goToButton = new qx.ui.form.Button().set({ - label: this.tr("Go to ") + cardName, - allowGrowX: false, - alignX: "right" - }); - goToButton.addListener("execute", () => this.fireEvent(signalName), this); - layout.add(goToButton); - - return layout; - }, - - __createWalletsView: function() { - const activeWallet = osparc.store.Store.getInstance().getActiveWallet(); - const preferredWallet = osparc.desktop.credits.Utils.getPreferredWallet(); - const oneWallet = activeWallet ? activeWallet : preferredWallet; - if (oneWallet) { - // show one wallet - return this.__showOneWallet(oneWallet); - } - // show some wallets - return this.__showSomeWallets(); - }, - - __showOneWallet: function(wallet) { - const layout = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); - - const titleLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)).set({ - alignY: "middle" - }); - const maxSize = 24; - // thumbnail or shared or not shared - const thumbnail = new qx.ui.basic.Image().set({ - backgroundColor: "transparent", - alignX: "center", - alignY: "middle", - scale: true, - allowShrinkX: true, - allowShrinkY: true, - maxHeight: maxSize, - maxWidth: maxSize - }); - const value = wallet.getThumbnail(); - if (value) { - thumbnail.setSource(value); - } else if (wallet.getAccessRights() && wallet.getAccessRights().length > 1) { - thumbnail.setSource(osparc.utils.Icons.organization(maxSize-4)); - } else { - thumbnail.setSource(osparc.utils.Icons.user(maxSize-4)); - } - titleLayout.add(thumbnail); - // name - const walletName = new qx.ui.basic.Label().set({ - font: "text-14", - alignY: "middle", - maxWidth: 200 - }); - wallet.bind("name", walletName, "value"); - titleLayout.add(walletName); - layout.add(titleLayout); - - const creditsIndicator = new osparc.desktop.credits.CreditsIndicator(wallet); - layout.add(creditsIndicator); - - const buyButton = new qx.ui.form.Button().set({ - label: this.tr("Buy Credits"), - icon: "@FontAwesome5Solid/dollar-sign/16", - maxHeight: 30, - alignY: "middle", - allowGrowX: false, - height: 25 - }); - const myAccessRights = wallet.getMyAccessRights(); - buyButton.setEnabled(Boolean(myAccessRights && myAccessRights["write"])); - buyButton.addListener("execute", () => this.fireDataEvent("buyCredits", { - walletId: wallet.getWalletId() - }), this); - layout.add(buyButton); - - return layout; - }, - - __showSomeWallets: function() { - const grid = new qx.ui.layout.Grid(12, 8); - const layout = new qx.ui.container.Composite(grid); - const maxWallets = 5; - const wallets = osparc.store.Store.getInstance().getWallets(); - for (let i=0; i 1) { - thumbnail.setSource(osparc.utils.Icons.organization(maxSize-4)); - } else { - thumbnail.setSource(osparc.utils.Icons.user(maxSize-4)); - } - layout.add(thumbnail, { - column, - row: i - }); - column++; - - // name - const walletName = new qx.ui.basic.Label().set({ - font: "text-14", - maxWidth: 100 - }); - wallet.bind("name", walletName, "value"); - layout.add(walletName, { - column, - row: i - }); - column++; - - // indicator - const creditsIndicator = new osparc.desktop.credits.CreditsIndicator(wallet); - layout.add(creditsIndicator, { - column, - row: i - }); - column++; - } - - return layout; - }, - - __createTransactionsView: function() { - const grid = new qx.ui.layout.Grid(12, 8); - const layout = new qx.ui.container.Composite(grid); - - const headers = [ - "Date", - "Price", - "Credits", - "Credit Account", - "Comment" - ]; - headers.forEach((header, column) => { - const text = new qx.ui.basic.Label(header).set({ - font: "text-14" - }); - layout.add(text, { - row: 0, - column - }); - }); - - osparc.data.Resources.fetch("payments", "get") - .then(transactions => { - if ("data" in transactions) { - const maxTransactions = 4; - transactions["data"].forEach((transaction, row) => { - if (row < maxTransactions) { - let walletName = null; - if (transaction["walletId"]) { - const found = osparc.desktop.credits.Utils.getWallet(transaction["walletId"]); - if (found) { - walletName = found.getName(); - } - } - const entry = [ - osparc.utils.Utils.formatDateAndTime(new Date(transaction["createdAt"])), - transaction["priceDollars"].toFixed(2).toString(), - transaction["osparcCredits"].toFixed(2).toString(), - walletName, - transaction["comment"] - ]; - entry.forEach((data, column) => { - const text = new qx.ui.basic.Label(data).set({ - font: "text-13" - }); - layout.add(text, { - row: row+1, - column - }); - }); - row++; - } - }); - } - }) - .catch(err => console.error(err)); - - return layout; - }, - - __createUsageView: function() { - const grid = new qx.ui.layout.Grid(12, 8); - const layout = new qx.ui.container.Composite(grid); - - const cols = osparc.resourceUsage.OverviewTable.COLUMNS; - const colNames = Object.values(cols).map(col => col.title); - colNames.forEach((colName, column) => { - const text = new qx.ui.basic.Label(colName).set({ - font: "text-14" - }); - layout.add(text, { - row: 0, - column - }); - }); - - const params = { - url: { - offset: 0, - limit: 4 // show only the last 4 usage - } - }; - osparc.data.Resources.fetch("resourceUsage", "getPage", params) - .then(async datas => { - const entries = await osparc.resourceUsage.OverviewTable.respDataToTableData(datas); - entries.forEach((entry, row) => { - entry.forEach((data, column) => { - const text = new qx.ui.basic.Label(data.toString()).set({ - font: "text-13" - }); - layout.add(text, { - row: row+1, - column - }); - }); - }); - }); - return layout; - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/PaymentGatewayWindow.js b/services/static-webserver/client/source/class/osparc/desktop/credits/PaymentGatewayWindow.js new file mode 100644 index 00000000000..b57f4e5536c --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/PaymentGatewayWindow.js @@ -0,0 +1,59 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.PaymentGatewayWindow", { + type: "static", + + statics: { + popUp: function(url, id, options, modal, useNativeModalDialog) { + const blocker = qx.bom.Window.getBlocker(); + blocker.setBlockerColor("#FFF"); + blocker.setBlockerOpacity(0.6); + + const pgWindow = qx.bom.Window.open( + url, + id, + options, + modal, + useNativeModalDialog + ); + + // enhance the blocker + const blockerDomEl = blocker.getBlockerElement(); + blockerDomEl.style.cursor = "pointer"; + + // text on blocker + const label = document.createElement("h1"); + label.innerHTML = "Don’t see the secure Payment Window?
Click here to complete your purchase"; + label.style.position = "fixed"; + const labelWidth = 550; + const labelHeight = 100; + label.style.width = labelWidth + "px"; + label.style.height = labelHeight + "px"; + const root = qx.core.Init.getApplication().getRoot(); + if (root && root.getBounds()) { + label.style.left = Math.round(root.getBounds().width/2) - labelWidth/2 + "px"; + label.style.top = Math.round(root.getBounds().height/2) - labelHeight/2 + "px"; + } + blockerDomEl.appendChild(label); + + blockerDomEl.addEventListener("click", () => pgWindow.focus()); + + return pgWindow; + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Summary.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Summary.js new file mode 100644 index 00000000000..77e2f61c3d2 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Summary.js @@ -0,0 +1,307 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.Summary", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(10)); + + this.__buildLayout(); + }, + + events: { + "buyCredits": "qx.event.type.Data", + "toWallets": "qx.event.type.Event", + "toActivity": "qx.event.type.Event" + }, + + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "wallets-card": { + const content = this.__createWalletsView(); + if (content) { + const wallets = osparc.store.Store.getInstance().getWallets(); + control = this.__createOverviewCard(this.tr("Credits Balance"), content, `All Credit Accounts (${wallets.length})`, "toWallets"); + this._add(control); + } + break; + } + case "settings-card": { + const content = this.__createSettingsView(); + const wallet = this.__getWallet(); + control = this.__createOverviewCard(this.tr("Settings"), content, this.tr("Credit Options"), "buyCredits", { + walletId: wallet ? wallet.getWalletId() : null + }); + this._add(control); + break; + } + case "activity-card": { + const content = this.__createActivityView(); + control = this.__createOverviewCard(this.tr("Last Activity"), content, this.tr("All Activity"), "toActivity"); + this._add(control); + break; + } + } + return control || this.base(arguments, id); + }, + + __buildLayout: function() { + this.getChildControl("wallets-card"); + this.getChildControl("settings-card"); + this.getChildControl("activity-card"); + }, + + __createOverviewCard: function(cardLabel, content, buttonLabel, signalName, signalData) { + const layout = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)).set({ + padding: 15, + backgroundColor: "background-main-1" + }); + layout.getContentElement().setStyles({ + "border-radius": "4px" + }); + + const topLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox()); + const title = new qx.ui.basic.Label().set({ + value: cardLabel, + font: "text-14", + allowGrowX: true + }); + topLayout.add(title, { + flex: 1 + }); + + const goToButton = new qx.ui.form.Button().set({ + label: buttonLabel, + allowGrowX: false, + alignX: "right" + }); + goToButton.addListener("execute", () => signalData ? this.fireDataEvent(signalName, signalData) : this.fireEvent(signalName), this); + topLayout.add(goToButton); + layout.add(topLayout); + + content.setPadding(5); + layout.add(content, { + flex: 1 + }); + + return layout; + }, + + __getWallet: function() { + const activeWallet = osparc.store.Store.getInstance().getActiveWallet(); + const preferredWallet = osparc.desktop.credits.Utils.getPreferredWallet(); + const wallet = activeWallet ? activeWallet : preferredWallet; + return wallet; + }, + + __createWalletsView: function() { + const wallet = this.__getWallet(); + if (wallet) { + // show one wallet + const layout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + + const titleLayout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)).set({ + alignY: "middle" + }); + const maxSize = 24; + // thumbnail or shared or not shared + const thumbnail = new qx.ui.basic.Image().set({ + backgroundColor: "transparent", + alignX: "center", + alignY: "middle", + scale: true, + allowShrinkX: true, + allowShrinkY: true, + maxHeight: maxSize, + maxWidth: maxSize + }); + const value = wallet.getThumbnail(); + if (value) { + thumbnail.setSource(value); + } else if (wallet.getAccessRights() && wallet.getAccessRights().length > 1) { + thumbnail.setSource(osparc.utils.Icons.organization(maxSize-4)); + } else { + thumbnail.setSource(osparc.utils.Icons.user(maxSize-4)); + } + titleLayout.add(thumbnail); + + // name + const walletName = new qx.ui.basic.Label().set({ + font: "text-14", + alignY: "middle" + }); + wallet.bind("name", walletName, "value"); + titleLayout.add(walletName); + layout.add(titleLayout); + + // credits indicator + const creditsIndicator = new osparc.desktop.credits.CreditsIndicator(wallet); + layout.add(creditsIndicator); + + // buy button + const buyButton = new qx.ui.form.Button().set({ + label: this.tr("Buy Credits"), + icon: "@FontAwesome5Solid/dollar-sign/16", + maxHeight: 30, + alignY: "middle", + allowGrowX: false, + height: 25 + }); + const myAccessRights = wallet.getMyAccessRights(); + buyButton.setEnabled(Boolean(myAccessRights && myAccessRights["write"])); + buyButton.addListener("execute", () => this.fireDataEvent("buyCredits", { + walletId: wallet.getWalletId() + }), this); + layout.add(buyButton); + + return layout; + } + return null; + }, + + __createSettingsView: function() { + const wallet = this.__getWallet(); + if (wallet) { + const grid = new qx.ui.layout.Grid(20, 10); + grid.setColumnAlign(0, "right", "middle"); + const layout = new qx.ui.container.Composite(grid); + + const t1t = new qx.ui.basic.Label(this.tr("Automatic Payment")).set({ + font: "text-14" + }); + layout.add(t1t, { + row: 0, + column: 0 + }); + const t1v = new qx.ui.basic.Label().set({ + value: "Off", + font: "text-14" + }); + layout.add(t1v, { + row: 0, + column: 1 + }); + + const t2t = new qx.ui.basic.Label(this.tr("Monthly Spending Limit")).set({ + font: "text-14" + }); + layout.add(t2t, { + row: 1, + column: 0 + }); + const t2v = new qx.ui.basic.Label().set({ + font: "text-14" + }); + layout.add(t2v, { + row: 1, + column: 1 + }); + + const t3t = new qx.ui.basic.Label(this.tr("Payment Method")).set({ + font: "text-14" + }); + layout.add(t3t, { + row: 2, + column: 0 + }); + const t3v = new qx.ui.basic.Label().set({ + font: "text-14" + }); + layout.add(t3v, { + row: 2, + column: 1 + }); + + wallet.bind("autoRecharge", t1v, "value", { + converter: arData => arData["enabled"] ? this.tr("On") : this.tr("Off") + }); + wallet.bind("autoRecharge", t2v, "value", { + converter: arData => arData["enabled"] ? (arData["topUpAmountInUsd"]*arData["topUpCountdown"]) + " US$" : null + }); + wallet.bind("autoRecharge", t3v, "value", { + converter: arData => arData["enabled"] ? arData["paymentMethodId"] : null + }); + + return layout; + } + return null; + }, + + __createActivityView: function() { + const grid = new qx.ui.layout.Grid(12, 8); + const layout = new qx.ui.container.Composite(grid); + + const cols = osparc.desktop.credits.ActivityTable.COLUMNS; + const colNames = Object.values(cols).map(col => col.title); + colNames.forEach((colName, column) => { + const text = new qx.ui.basic.Label(colName).set({ + font: "text-13" + }); + layout.add(text, { + row: 0, + column + }); + }); + + const walletId = this.__getWallet().getWalletId(); + const params = { + url: { + walletId: walletId, + offset: 0, + limit: 10 + } + }; + Promise.all([ + osparc.data.Resources.fetch("resourceUsagePerWallet", "getPage", params), + osparc.data.Resources.fetch("payments", "get") + ]) + .then(responses => { + const usages = responses[0]; + const transactions = responses[1]["data"]; + const activities1 = osparc.desktop.credits.ActivityTable.usagesToActivities(usages); + // Filter out some transactions + const filteredTransactions = transactions.filter(transaction => transaction["completedStatus"] !== "FAILED" && transaction["walletId"] === walletId); + const activities2 = osparc.desktop.credits.ActivityTable.transactionsToActivities(filteredTransactions); + const activities = activities1.concat(activities2); + activities.sort((a, b) => new Date(b["date"]).getTime() - new Date(a["date"]).getTime()); + + const maxRows = 6; + const entries = osparc.desktop.credits.ActivityTable.respDataToTableData(activities); + entries.forEach((entry, row) => { + if (row >= maxRows) { + return; + } + entry.forEach((data, column) => { + const text = new qx.ui.basic.Label(data.toString()).set({ + font: "text-14" + }); + layout.add(text, { + row: row+1, + column + }); + }); + }); + }); + return layout; + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Transactions.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Transactions.js index afbe36f5cab..ee55313d066 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/Transactions.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Transactions.js @@ -21,132 +21,36 @@ qx.Class.define("osparc.desktop.credits.Transactions", { construct: function() { this.base(arguments); - this._setLayout(new qx.ui.layout.VBox(20)); - - this.__buildLayout(); + this._setLayout(new qx.ui.layout.VBox(15)); + + const transactionsTable = this.getChildControl("transactions-table"); + osparc.data.Resources.fetch("payments", "get") + .then(transactions => { + if ("data" in transactions) { + transactionsTable.addData(transactions["data"]); + } + }) + .catch(err => console.error(err)); }, - statics: { - COLUMNS: { - date: { - pos: 0, - title: qx.locale.Manager.tr("Date") - }, - price: { - pos: 1, - title: qx.locale.Manager.tr("Price") - }, - credits: { - pos: 2, - title: qx.locale.Manager.tr("Credits") - }, - wallet: { - pos: 3, - title: qx.locale.Manager.tr("Credit Account") - }, - status: { - pos: 4, - title: qx.locale.Manager.tr("Status") - }, - comment: { - pos: 5, - title: qx.locale.Manager.tr("Comment") - }, - invoice: { - pos: 6, - title: qx.locale.Manager.tr("Invoice") - } - }, - - addColorTag: function(status) { - const color = this.getLevelColor(status); - status = osparc.utils.Utils.onlyFirstsUp(status); - return ("" + status + ""); - }, - - getLevelColor: function(status) { - const colorManager = qx.theme.manager.Color.getInstance(); - let logLevel = null; - switch (status) { - case "SUCCESS": - logLevel = "info"; - break; - case "PENDING": - logLevel = "warning"; - break; - case "CANCELED": - case "FAILED": - logLevel = "error"; - break; - default: - console.error("completedStatus unknown"); + members: { + _createChildControlImpl: function(id) { + let control; + switch (id) { + case "transactions-table": + control = new osparc.desktop.credits.TransactionsTable(); + this._add(control); break; } - return colorManager.resolve("logger-"+logLevel+"-message"); - }, - - createPdfIconWithLink: function(link) { - return `Invoice`; - }, - - respDataToTableData: function(datas) { - const newDatas = []; - if (datas) { - const cols = this.COLUMNS; - datas.forEach(data => { - const newData = []; - newData[cols["date"].pos] = osparc.utils.Utils.formatDateAndTime(new Date(data["createdAt"])); - newData[cols["price"].pos] = data["priceDollars"] ? data["priceDollars"] : 0; - newData[cols["credits"].pos] = data["osparcCredits"] ? data["osparcCredits"] : 0; - let walletName = "Unknown"; - const found = osparc.desktop.credits.Utils.getWallet(data["walletId"]); - if (found) { - walletName = found.getName(); - } - newData[cols["wallet"].pos] = walletName; - if (data["completedStatus"]) { - newData[cols["status"].pos] = this.addColorTag(data["completedStatus"]); - } - newData[cols["comment"].pos] = data["comment"]; - const invoiceUrl = data["invoiceUrl"]; - newData[cols["invoice"].pos] = invoiceUrl? this.createPdfIconWithLink(invoiceUrl) : ""; - - newDatas.push(newData); - }); - } - return newDatas; - } - }, - - members: { - __table: null, - - __buildLayout: function() { - const tableModel = new qx.ui.table.model.Simple(); - const cols = this.self().COLUMNS; - const colNames = Object.values(cols).map(col => col.title); - tableModel.setColumns(colNames); - - const table = this.__table = new osparc.ui.table.Table(tableModel, { - tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj) - }); - const htmlRenderer = new qx.ui.table.cellrenderer.Html(); - table.getTableColumnModel().setDataCellRenderer(cols.status.pos, htmlRenderer); - table.getTableColumnModel().setDataCellRenderer(cols.invoice.pos, htmlRenderer); - table.setColumnWidth(cols.invoice.pos, 50); - table.makeItLoose(); - this._add(table); - - this.refetchData(); + return control || this.base(arguments, id); }, - refetchData: function() { - this.__table.setData([]); + __fetchData: function() { osparc.data.Resources.fetch("payments", "get") .then(transactions => { if ("data" in transactions) { - const newDatas = this.self().respDataToTableData(transactions["data"]); - this.__table.setData(newDatas); + const table = this.getChildControl("usage-table"); + table.addData(transactions["data"]); } }) .catch(err => console.error(err)); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/TransactionsTable.js b/services/static-webserver/client/source/class/osparc/desktop/credits/TransactionsTable.js new file mode 100644 index 00000000000..62b171cbdd1 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/TransactionsTable.js @@ -0,0 +1,143 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.TransactionsTable", { + extend: osparc.ui.table.Table, + + construct: function() { + const model = new qx.ui.table.model.Simple(); + const cols = this.self().COLUMNS; + const colNames = Object.values(cols).map(col => col.title); + model.setColumns(colNames); + + this.base(arguments, model, { + tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj), + statusBarVisible: false + }); + + const htmlRenderer = new qx.ui.table.cellrenderer.Html(); + const columnModel = this.getTableColumnModel(); + columnModel.setDataCellRenderer(cols.status.pos, htmlRenderer); + columnModel.setDataCellRenderer(cols.invoice.pos, htmlRenderer); + this.setColumnWidth(cols.invoice.pos, 50); + this.makeItLoose(); + }, + + statics: { + COLUMNS: { + date: { + pos: 0, + title: qx.locale.Manager.tr("Date") + }, + price: { + pos: 1, + title: qx.locale.Manager.tr("Price") + }, + credits: { + pos: 2, + title: qx.locale.Manager.tr("Credits") + }, + wallet: { + pos: 3, + title: qx.locale.Manager.tr("Credit Account") + }, + status: { + pos: 4, + title: qx.locale.Manager.tr("Status") + }, + comment: { + pos: 5, + title: qx.locale.Manager.tr("Comment") + }, + invoice: { + pos: 6, + title: qx.locale.Manager.tr("Invoice") + } + }, + + addColorTag: function(status) { + const color = this.getLevelColor(status); + status = osparc.utils.Utils.onlyFirstsUp(status); + return ("" + status + ""); + }, + + getLevelColor: function(status) { + const colorManager = qx.theme.manager.Color.getInstance(); + let logLevel = null; + switch (status) { + case "SUCCESS": + logLevel = "info"; + break; + case "PENDING": + logLevel = "warning"; + break; + case "CANCELED": + case "FAILED": + logLevel = "error"; + break; + default: + console.error("completedStatus unknown"); + break; + } + return colorManager.resolve("logger-"+logLevel+"-message"); + }, + + createPdfIconWithLink: function(link) { + return `Invoice`; + }, + + respDataToTableRow: function(data) { + const cols = this.COLUMNS; + const newData = []; + newData[cols["date"].pos] = osparc.utils.Utils.formatDateAndTime(new Date(data["createdAt"])); + newData[cols["price"].pos] = data["priceDollars"] ? data["priceDollars"] : 0; + newData[cols["credits"].pos] = data["osparcCredits"] ? data["osparcCredits"] : 0; + let walletName = "Unknown"; + const found = osparc.desktop.credits.Utils.getWallet(data["walletId"]); + if (found) { + walletName = found.getName(); + } + newData[cols["wallet"].pos] = walletName; + if (data["completedStatus"]) { + newData[cols["status"].pos] = this.addColorTag(data["completedStatus"]); + } + newData[cols["comment"].pos] = data["comment"]; + const invoiceUrl = data["invoiceUrl"]; + newData[cols["invoice"].pos] = invoiceUrl? this.createPdfIconWithLink(invoiceUrl) : ""; + + return newData; + }, + + respDataToTableData: function(datas) { + const newDatas = []; + if (datas) { + datas.forEach(data => { + const newData = this.respDataToTableRow(data); + newDatas.push(newData); + }); + } + return newDatas; + } + }, + + members: { + addData: function(datas) { + const newDatas = this.self().respDataToTableData(datas); + this.setData(newDatas); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/resourceUsage/Overview.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js similarity index 75% rename from services/static-webserver/client/source/class/osparc/resourceUsage/Overview.js rename to services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js index 7b51b7ba9df..10116bf7707 100644 --- a/services/static-webserver/client/source/class/osparc/resourceUsage/Overview.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Usage.js @@ -15,7 +15,7 @@ ************************************************************************ */ -qx.Class.define("osparc.resourceUsage.Overview", { +qx.Class.define("osparc.desktop.credits.Usage", { extend: qx.ui.core.Widget, construct: function() { @@ -23,13 +23,8 @@ qx.Class.define("osparc.resourceUsage.Overview", { this._setLayout(new qx.ui.layout.VBox(15)); - this.getChildControl("wallet-selector-title"); - const walletSelector = this.getChildControl("wallet-selector"); - this.getChildControl("wallet-selector-layout").exclude(); - const walletsEnabled = osparc.desktop.credits.Utils.areWalletsEnabled(); - if (walletsEnabled) { - this.getChildControl("wallet-selector-layout").show(); - } + const walletSelectorLayout = this.getChildControl("wallet-selector-layout"); + const walletSelector = walletSelectorLayout.getChildren()[1]; const loadingImage = this.getChildControl("loading-image"); loadingImage.show(); @@ -45,18 +40,7 @@ qx.Class.define("osparc.resourceUsage.Overview", { }, statics: { - ITEMS_PER_PAGE: 15, - - popUpInWindow: function() { - const title = qx.locale.Manager.tr("Usage"); - const noteEditor = new osparc.resourceUsage.Overview(); - const viewWidth = 900; - const viewHeight = 450; - const win = osparc.ui.window.Window.popUpInWindow(noteEditor, title, viewWidth, viewHeight); - win.center(); - win.open(); - return win; - } + ITEMS_PER_PAGE: 15 }, members: { @@ -67,27 +51,9 @@ qx.Class.define("osparc.resourceUsage.Overview", { let control; switch (id) { case "wallet-selector-layout": - control = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + control = osparc.desktop.credits.Utils.createWalletSelectorLayout("read"); this._add(control); break; - case "wallet-selector-title": { - control = new qx.ui.basic.Label(this.tr("Select Credit Account")).set({ - alignY: "middle" - }); - const layout = this.getChildControl("wallet-selector-layout"); - layout.add(control); - break; - } - case "wallet-selector": { - control = osparc.desktop.credits.Utils.createWalletSelector("read", false, true).set({ - allowGrowX: false - }); - // select "All Credit Accounts" by default - control.getSelectables()[0].setLabel("All Credit Accounts"); - const layout = this.getChildControl("wallet-selector-layout"); - layout.add(control); - break; - } case "loading-image": control = new qx.ui.basic.Image().set({ source: "@FontAwesome5Solid/circle-notch/64", @@ -98,7 +64,7 @@ qx.Class.define("osparc.resourceUsage.Overview", { this._add(control); break; case "usage-table": - control = new osparc.resourceUsage.OverviewTable().set({ + control = new osparc.desktop.credits.UsageTable().set({ height: (this.self().ITEMS_PER_PAGE*20 + 40) }); this._add(control); @@ -171,8 +137,8 @@ qx.Class.define("osparc.resourceUsage.Overview", { __getPrevRequest: function() { const params = { url: { - offset: osparc.resourceUsage.Overview.ITEMS_PER_PAGE, - limit: osparc.resourceUsage.Overview.ITEMS_PER_PAGE + offset: this.self().ITEMS_PER_PAGE, + limit: this.self().ITEMS_PER_PAGE } }; if (this.__prevRequestParams) { @@ -186,7 +152,7 @@ qx.Class.define("osparc.resourceUsage.Overview", { const params = { url: { offset: 0, - limit: osparc.resourceUsage.Overview.ITEMS_PER_PAGE + limit: this.self().ITEMS_PER_PAGE } }; if (this.__nextRequestParams) { @@ -201,13 +167,15 @@ qx.Class.define("osparc.resourceUsage.Overview", { resolveWResponse: true }; - const walletSelector = this.getChildControl("wallet-selector"); + const walletSelectorLayout = this.getChildControl("wallet-selector-layout"); + const walletSelector = walletSelectorLayout.getChildren()[1]; const walletSelection = walletSelector.getSelection(); const walletId = walletSelection && walletSelection.length ? walletSelection[0].walletId : null; if (walletId) { params.url["walletId"] = walletId.toString(); return osparc.data.Resources.fetch("resourceUsagePerWallet", "getPage", params, undefined, options); } + // Usage supports the non wallet enabled products return osparc.data.Resources.fetch("resourceUsage", "getPage", params, undefined, options); }, diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js b/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js new file mode 100644 index 00000000000..40663635dbf --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/UsageTable.js @@ -0,0 +1,125 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.desktop.credits.UsageTable", { + extend: osparc.ui.table.Table, + + construct: function() { + const model = new qx.ui.table.model.Simple(); + const cols = this.self().COLUMNS; + const colNames = Object.values(cols).map(col => col.title); + model.setColumns(colNames); + + this.base(arguments, model, { + tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj), + statusBarVisible: false + }); + this.makeItLoose(); + + const columnModel = this.getTableColumnModel(); + columnModel.getBehavior().setWidth(this.self().COLUMNS.duration.pos, 70); + columnModel.getBehavior().setWidth(this.self().COLUMNS.status.pos, 70); + columnModel.getBehavior().setWidth(this.self().COLUMNS.cost.pos, 60); + + if (!osparc.desktop.credits.Utils.areWalletsEnabled()) { + columnModel.setColumnVisible(this.self().COLUMNS.user.pos, false); + } + }, + + statics: { + COLUMNS: { + project: { + pos: 0, + title: osparc.product.Utils.getStudyAlias({firstUpperCase: true}) + }, + node: { + pos: 1, + title: qx.locale.Manager.tr("Node") + }, + service: { + pos: 2, + title: qx.locale.Manager.tr("Service") + }, + start: { + pos: 3, + title: qx.locale.Manager.tr("Start") + }, + duration: { + pos: 4, + title: qx.locale.Manager.tr("Duration") + }, + status: { + pos: 5, + title: qx.locale.Manager.tr("Status") + }, + cost: { + pos: 6, + title: qx.locale.Manager.tr("Cost") + }, + user: { + pos: 7, + title: qx.locale.Manager.tr("User") + } + }, + + respDataToTableRow: async function(data) { + const cols = this.COLUMNS; + const newData = []; + newData[cols["project"].pos] = data["project_name"] ? data["project_name"] : data["project_id"]; + newData[cols["node"].pos] = data["node_name"] ? data["node_name"] : data["node_id"]; + if (data["service_key"]) { + const parts = data["service_key"].split("/"); + const serviceName = parts.pop(); + newData[cols["service"].pos] = serviceName + ":" + data["service_version"]; + } + if (data["started_at"]) { + const startTime = new Date(data["started_at"]); + newData[cols["start"].pos] = osparc.utils.Utils.formatDateAndTime(startTime); + if (data["stopped_at"]) { + const stopTime = new Date(data["stopped_at"]); + const durationTime = stopTime - startTime; + newData[cols["duration"].pos] = osparc.utils.Utils.formatMilliSeconds(durationTime); + } + } + newData[cols["status"].pos] = qx.lang.String.firstUp(data["service_run_status"].toLowerCase()); + newData[cols["cost"].pos] = data["credit_cost"] ? data["credit_cost"] : "-"; + const user = await osparc.store.Store.getInstance().getUser(data["user_id"]); + if (user) { + newData[cols["user"].pos] = user ? user["label"] : data["user_id"]; + } + return newData; + }, + + respDataToTableData: async function(datas) { + const newDatas = []; + if (datas) { + for (const data of datas) { + const newData = await this.respDataToTableRow(data); + newDatas.push(newData); + } + } + return newDatas; + } + }, + + members: { + addData: async function(datas) { + const newDatas = await this.self().respDataToTableData(datas); + this.setData(newDatas); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenter.js b/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenter.js index 4ed000d1636..5d378b1d013 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenter.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenter.js @@ -49,14 +49,19 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { tabViews.add(walletsPage); } + if (this.__walletsEnabled) { + const paymentMethodsPage = this.__paymentMethodsPage = this.__getPaymentMethodsPage(); + tabViews.add(paymentMethodsPage); + } + if (this.__walletsEnabled) { const buyCreditsPage = this.__buyCreditsPage = this.__getBuyCreditsPage(); tabViews.add(buyCreditsPage); } if (this.__walletsEnabled) { - const paymentMethodsPage = this.__paymentMethodsPage = this.__getPaymentMethodsPage(); - tabViews.add(paymentMethodsPage); + const activityPage = this.__activityPage = this.__getActivityPage(); + tabViews.add(activityPage); } if (this.__walletsEnabled) { @@ -133,17 +138,18 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { __walletsPage: null, __buyCreditsPage: null, __paymentMethodsPage: null, + __activityPage: null, __transactionsPage: null, __usageOverviewPage: null, __buyCredits: null, __transactionsTable: null, __getOverviewPage: function() { - const title = this.tr("Overview"); + const title = this.tr("Summary"); const iconSrc = "@FontAwesome5Solid/table/22"; const page = new osparc.desktop.preferences.pages.BasePage(title, iconSrc); page.showLabelOnTab(); - const overview = new osparc.desktop.credits.Overview(); + const overview = new osparc.desktop.credits.Summary(); overview.set({ margin: 10 }); @@ -159,6 +165,7 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { } }); overview.addListener("toWallets", () => this.openWallets()); + overview.addListener("toActivity", () => this.__openActivity()); overview.addListener("toTransactions", () => this.__openTransactions()); overview.addListener("toUsageOverview", () => this.__openUsageOverview()); page.add(overview); @@ -195,6 +202,19 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { return page; }, + __getPaymentMethodsPage: function() { + const title = this.tr("Payment Methods"); + const iconSrc = "@FontAwesome5Solid/credit-card/22"; + const page = new osparc.desktop.preferences.pages.BasePage(title, iconSrc); + page.showLabelOnTab(); + const paymentMethods = new osparc.desktop.paymentMethods.PaymentMethods(); + paymentMethods.set({ + margin: 10 + }); + page.add(paymentMethods); + return page; + }, + __getBuyCreditsPage: function() { const title = this.tr("Buy Credits"); const iconSrc = "@FontAwesome5Solid/dollar-sign/22"; @@ -209,16 +229,16 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { return page; }, - __getPaymentMethodsPage: function() { - const title = this.tr("Payment Methods"); - const iconSrc = "@FontAwesome5Solid/credit-card/22"; + __getActivityPage: function() { + const title = this.tr("Activity"); + const iconSrc = "@FontAwesome5Solid/list/22"; const page = new osparc.desktop.preferences.pages.BasePage(title, iconSrc); page.showLabelOnTab(); - const paymentMethods = new osparc.desktop.paymentMethods.PaymentMethods(); - paymentMethods.set({ + const activity = new osparc.desktop.credits.Activity(); + activity.set({ margin: 10 }); - page.add(paymentMethods); + page.add(activity); return page; }, @@ -240,7 +260,7 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { const iconSrc = "@FontAwesome5Solid/list/22"; const page = new osparc.desktop.preferences.pages.BasePage(title, iconSrc); page.showLabelOnTab(); - const usageOverview = new osparc.resourceUsage.Overview(); + const usageOverview = new osparc.desktop.credits.Usage(); usageOverview.set({ margin: 10 }); @@ -283,6 +303,10 @@ qx.Class.define("osparc.desktop.credits.UserCenter", { this.__openPage(this.__buyCreditsPage); }, + __openActivity: function() { + this.__openPage(this.__activityPage); + }, + __openTransactions: function(fetchTransactions = false) { if (fetchTransactions) { this.__transactionsTable.refetchData(); diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenterWindow.js b/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenterWindow.js index 2c8bbf78aed..ca0f45da509 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenterWindow.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/UserCenterWindow.js @@ -21,7 +21,7 @@ qx.Class.define("osparc.desktop.credits.UserCenterWindow", { construct: function(walletsEnabled = false) { this.base(arguments, "credits", this.tr("User Center")); - const viewWidth = walletsEnabled ? 1050 : 800; + const viewWidth = walletsEnabled ? 1000 : 800; const viewHeight = walletsEnabled ? 700 : 600; this.set({ diff --git a/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js b/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js index 897e78c8cf5..d4c16c399e5 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js +++ b/services/static-webserver/client/source/class/osparc/desktop/credits/Utils.js @@ -64,6 +64,24 @@ qx.Class.define("osparc.desktop.credits.Utils", { return walletSelector; }, + createWalletSelectorLayout: function(accessRight = "read") { + const layout = new qx.ui.container.Composite(new qx.ui.layout.HBox(10)); + + const label = new qx.ui.basic.Label(qx.locale.Manager.tr("Select Credit Account")); + layout.add(label); + + const walletSelector = osparc.desktop.credits.Utils.createWalletSelector(accessRight); + layout.add(walletSelector); + + if (osparc.desktop.credits.Utils.areWalletsEnabled() && walletSelector.getSelectables().length > 1) { + layout.show(); + } else { + layout.exclude(); + } + + return layout; + }, + autoSelectActiveWallet: function(walletSelector) { // If there is only one active wallet, select it const store = osparc.store.Store.getInstance(); @@ -89,6 +107,16 @@ qx.Class.define("osparc.desktop.credits.Utils", { return null; }, + getMyWallets: function() { + const store = osparc.store.Store.getInstance(); + const wallets = store.getWallets(); + const myWallets = wallets.filter(wallet => wallet.getMyAccessRights()["write"]); + if (myWallets) { + return myWallets; + } + return []; + }, + getPreferredWallet: function() { const store = osparc.store.Store.getInstance(); const wallets = store.getWallets(); @@ -110,8 +138,7 @@ qx.Class.define("osparc.desktop.credits.Utils", { }; promises.push(osparc.data.Resources.fetch("paymentMethods", "get", params)); } else { - const wallets = osparc.store.Store.getInstance().getWallets(); - const myWallets = wallets.filter(wallet => wallet.getMyAccessRights()["write"]); + const myWallets = this.getMyWallets(); myWallets.forEach(myWallet => { const params = { url: { diff --git a/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethodListItem.js b/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethodListItem.js index cdf335d9a9b..61cb2b28f21 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethodListItem.js +++ b/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethodListItem.js @@ -16,19 +16,21 @@ ************************************************************************ */ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { - extend: osparc.ui.list.ListItemWithMenu, + extend: osparc.ui.list.ListItem, construct: function() { this.base(arguments); const layout = this._getLayout(); layout.setSpacingX(15); - layout.setColumnFlex(1, 0); - layout.setColumnFlex(2, 0); - layout.setColumnFlex(3, 0); - layout.setColumnFlex(4, 0); - layout.setColumnFlex(5, 1); - layout.setColumnFlex(6, 0); + layout.setColumnFlex(this.self().GRID_POS.ICON, 0); + layout.setColumnFlex(this.self().GRID_POS.NAME, 0); + layout.setColumnFlex(this.self().GRID_POS.TYPE, 0); + layout.setColumnFlex(this.self().GRID_POS.MASKED_NUMBER, 0); + layout.setColumnFlex(this.self().GRID_POS.EXPIRATION_DATE, 1); + // buttons to the right + layout.setColumnFlex(this.self().GRID_POS.INFO_BUTTON, 0); + layout.setColumnFlex(this.self().GRID_POS.DELETE_BUTTON, 0); this.getChildControl("thumbnail").setSource("@FontAwesome5Solid/credit-card/18"); @@ -49,16 +51,8 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { converter: year => this.getExpirationMonth() + "/" + year }); - const store = osparc.store.Store.getInstance(); - const walletName = this.getChildControl("wallet-name"); - this.bind("walletId", walletName, "value", { - converter: walletId => { - const found = store.getWallets().find(wallet => wallet.getWalletId() === walletId); - return found ? found.getName() : this.tr("Unknown Credit Account"); - } - }); - - this.__getOptionsMenu(); + this.getChildControl("details-button"); + this.getChildControl("delete-button"); }, properties: { @@ -110,6 +104,18 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { "deletePaymentMethod": "qx.event.type.Data" }, + statics: { + GRID_POS: { + ICON: 0, + NAME: 1, + TYPE: 2, + MASKED_NUMBER: 3, + EXPIRATION_DATE: 4, + INFO_BUTTON: 5, + DELETE_BUTTON: 6 + } + }, + members: { _createChildControlImpl: function(id) { let control; @@ -120,8 +126,7 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { }); this._add(control, { row: 0, - column: 1, - rowSpan: 2 + column: this.self().GRID_POS.NAME }); break; case "card-type": @@ -130,8 +135,7 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { }); this._add(control, { row: 0, - column: 2, - rowSpan: 2 + column: this.self().GRID_POS.TYPE }); break; case "card-number-masked": @@ -140,8 +144,7 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { }); this._add(control, { row: 0, - column: 3, - rowSpan: 2 + column: this.self().GRID_POS.MASKED_NUMBER }); break; case "expiration-date": @@ -150,75 +153,47 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethodListItem", { }); this._add(control, { row: 0, - column: 4, - rowSpan: 2 + column: this.self().GRID_POS.EXPIRATION_DATE }); break; - case "wallet-name": - control = new qx.ui.basic.Label().set({ - font: "text-14" + case "details-button": + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/info/14" }); + control.addListener("execute", () => this.fireDataEvent("openPaymentMethodDetails", this.getKey())); this._add(control, { row: 0, - column: 5, - rowSpan: 2 + column: this.self().GRID_POS.INFO_BUTTON }); break; - case "options-menu": { - const iconSize = 26; - control = new qx.ui.form.MenuButton().set({ - maxWidth: iconSize, - maxHeight: iconSize, - alignX: "center", - alignY: "middle", - icon: "@FontAwesome5Solid/ellipsis-v/"+(iconSize-11), - focusable: false + case "delete-button": + control = new qx.ui.form.Button().set({ + icon: "@FontAwesome5Solid/trash/14" }); + control.addListener("execute", () => this.__deletePressed()); this._add(control, { row: 0, - column: 6, - rowSpan: 2 + column: this.self().GRID_POS.DELETE_BUTTON }); break; - } } return control || this.base(arguments, id); }, - // overridden - __getOptionsMenu: function() { - const optionsMenu = this.getChildControl("options-menu"); - optionsMenu.show(); - - const menu = new qx.ui.menu.Menu().set({ - position: "bottom-right" + __deletePressed: function() { + const msg = this.tr("Are you sure you want to delete the Payment Method?"); + const win = new osparc.ui.window.Confirmation(msg).set({ + confirmText: this.tr("Delete"), + confirmAction: "delete" + }); + win.center(); + win.open(); + win.addListener("close", () => { + if (win.getConfirmed()) { + this.fireDataEvent("deletePaymentMethod", this.getKey()); + } }); - - const viewDetailsButton = new qx.ui.menu.Button(this.tr("View details...")); - viewDetailsButton.addListener("execute", () => this.fireDataEvent("openPaymentMethodDetails", this.getKey())); - menu.add(viewDetailsButton); - - const detelePMButton = new qx.ui.menu.Button(this.tr("Delete Payment Method")); - detelePMButton.addListener("execute", () => { - const msg = this.tr("Are you sure you want to delete the Payment Method?"); - const win = new osparc.ui.window.Confirmation(msg).set({ - confirmText: this.tr("Delete"), - confirmAction: "delete" - }); - win.center(); - win.open(); - win.addListener("close", () => { - if (win.getConfirmed()) { - this.fireDataEvent("deletePaymentMethod", this.getKey()); - } - }); - }, this); - menu.add(detelePMButton); - - optionsMenu.setMenu(menu); - - return menu; } } }); diff --git a/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethods.js b/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethods.js index bce3dbdaffe..e8c59ab2506 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethods.js +++ b/services/static-webserver/client/source/class/osparc/desktop/paymentMethods/PaymentMethods.js @@ -24,10 +24,13 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethods", { this._setLayout(new qx.ui.layout.VBox(20)); this.getChildControl("intro-text"); - this.getChildControl("add-payment-methods-button"); + const walletSelectorLayout = this.getChildControl("wallet-selector-layout"); + const walletSelector = walletSelectorLayout.getChildren()[1]; this.getChildControl("payment-methods-list-layout"); + this.getChildControl("add-payment-methods-button"); this.__fetchPaymentMethods(); + walletSelector.addListener("changeSelection", () => this.__fetchPaymentMethods()); }, properties: { @@ -47,13 +50,23 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethods", { switch (id) { case "intro-text": control = new qx.ui.basic.Label().set({ - value: this.tr("Intro text about payment methods"), + value: this.tr("Credit cards used for payments in your personal Credit Account"), font: "text-14", rich: true, wrap: true }); this._add(control); break; + case "wallet-selector-layout": + control = osparc.desktop.credits.Utils.createWalletSelectorLayout("write"); + this._add(control); + break; + case "payment-methods-list-layout": + control = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); + this._add(control, { + flex: 1 + }); + break; case "add-payment-methods-button": control = new qx.ui.form.Button().set({ appearance: "strong-button", @@ -64,21 +77,22 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethods", { control.addListener("execute", () => this.__addNewPaymentMethod(), this); this._add(control); break; - case "payment-methods-list-layout": - control = new qx.ui.container.Composite(new qx.ui.layout.VBox(10)); - this._add(control); - break; } return control || this.base(arguments, id); }, + __getSelectedWalletId: function() { + const walletSelector = this.getChildControl("wallet-selector-layout").getChildren()[1]; + const walletSelection = walletSelector.getSelection(); + return walletSelection && walletSelection.length ? walletSelection[0].walletId : null; + }, + __addNewPaymentMethod: function() { - const wallets = osparc.store.Store.getInstance().getWallets(); - const myWallet = wallets.find(wallet => wallet.getMyAccessRights()["write"]); - if (myWallet) { + const walletId = this.__getSelectedWalletId(); + if (walletId) { const params = { url: { - walletId: myWallet.getWalletId() + walletId: walletId } }; osparc.data.Resources.fetch("paymentMethods", "init", params) @@ -90,32 +104,25 @@ qx.Class.define("osparc.desktop.paymentMethods.PaymentMethods", { const listLayout = this.getChildControl("payment-methods-list-layout"); listLayout.removeAll(); - listLayout.add(new qx.ui.basic.Label().set({ - value: this.tr("Fetching Payment Methods"), - font: "text-14", - rich: true, - wrap: true - })); - - const promises = []; - const wallets = osparc.store.Store.getInstance().getWallets(); - wallets.forEach(wallet => { - if (wallet.getMyAccessRights() && wallet.getMyAccessRights()["write"]) { - const params = { - url: { - walletId: wallet.getWalletId() - } - }; - promises.push(osparc.data.Resources.fetch("paymentMethods", "get", params)); - } + const fetchingLabel = new qx.ui.basic.Atom().set({ + label: this.tr("Fetching Payment Methods"), + icon: "@FontAwesome5Solid/circle-notch/12", + font: "text-14" }); - Promise.all(promises) + fetchingLabel.getChildControl("icon").getContentElement().addClass("rotate"); + listLayout.add(fetchingLabel); + + const walletId = this.__getSelectedWalletId(); + const params = { + url: { + walletId: walletId + } + }; + osparc.data.Resources.fetch("paymentMethods", "get", params) .then(paymentMethods => { - const allPaymentMethods = []; - paymentMethods.forEach(paymentMethod => allPaymentMethods.push(...paymentMethod)); listLayout.removeAll(); - if (allPaymentMethods.length) { - listLayout.add(this.__populatePaymentMethodsList(allPaymentMethods)); + if (paymentMethods.length) { + listLayout.add(this.__populatePaymentMethodsList(paymentMethods)); } else { listLayout.add(new qx.ui.basic.Label().set({ value: this.tr("No Payment Methods found"), diff --git a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletDetails.js b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletDetails.js index b6fbe4dd3e7..541b76a380c 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletDetails.js +++ b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletDetails.js @@ -94,8 +94,7 @@ qx.Class.define("osparc.desktop.wallets.WalletDetails", { __openEditWallet: function() { const wallet = this.__walletModel; - const newWallet = false; - const walletEditor = new osparc.desktop.wallets.WalletEditor(newWallet); + const walletEditor = new osparc.desktop.wallets.WalletEditor(); wallet.bind("walletId", walletEditor, "walletId"); wallet.bind("name", walletEditor, "name"); wallet.bind("description", walletEditor, "description"); @@ -127,14 +126,8 @@ qx.Class.define("osparc.desktop.wallets.WalletDetails", { osparc.data.Resources.fetch("wallets", "put", params) .then(() => { osparc.FlashMessenger.getInstance().logAs(name + this.tr(" successfully edited")); - osparc.store.Store.getInstance().invalidate("wallets"); - const store = osparc.store.Store.getInstance(); - store.reloadWallets(); - this.__walletModel.set({ - name: name, - description: description, - thumbnail: thumbnail || null - }); + const wallet = osparc.desktop.credits.Utils.getWallet(walletId); + wallet.set(params.data); }) .catch(err => { console.error(err); diff --git a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletEditor.js b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletEditor.js index 44ab7cf8d3a..299648f007b 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletEditor.js +++ b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletEditor.js @@ -18,7 +18,7 @@ qx.Class.define("osparc.desktop.wallets.WalletEditor", { extend: qx.ui.core.Widget, - construct: function(newWallet = true) { + construct: function() { this.base(arguments); this._setLayout(new qx.ui.layout.VBox(8)); @@ -29,7 +29,7 @@ qx.Class.define("osparc.desktop.wallets.WalletEditor", { manager.add(title); this.getChildControl("description"); this.getChildControl("thumbnail"); - newWallet ? this.getChildControl("create") : this.getChildControl("save"); + this.getChildControl("save"); }, properties: { @@ -63,7 +63,6 @@ qx.Class.define("osparc.desktop.wallets.WalletEditor", { }, events: { - "createWallet": "qx.event.type.Event", "updateWallet": "qx.event.type.Event", "cancel": "qx.event.type.Event" }, @@ -108,20 +107,6 @@ qx.Class.define("osparc.desktop.wallets.WalletEditor", { this._add(control); break; } - case "create": { - const buttons = this.getChildControl("buttonsLayout"); - control = new osparc.ui.form.FetchButton(this.tr("Create")).set({ - appearance: "strong-button" - }); - control.addListener("execute", () => { - if (this.__validator.validate()) { - control.setFetching(true); - this.fireEvent("createWallet"); - } - }, this); - buttons.addAt(control, 0); - break; - } case "save": { const buttons = this.getChildControl("buttonsLayout"); control = new osparc.ui.form.FetchButton(this.tr("Save")); diff --git a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletsList.js b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletsList.js index e94f4652f68..118f1f944d8 100644 --- a/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletsList.js +++ b/services/static-webserver/client/source/class/osparc/desktop/wallets/WalletsList.js @@ -38,13 +38,6 @@ qx.Class.define("osparc.desktop.wallets.WalletsList", { flex: 1 }); - // Disabled until create a New Wallet is enabled - /* - const newWalletButton = this.__getCreateWalletSection(); - newWalletButton.setVisibility(osparc.data.Permissions.getInstance().canDo("user.wallets.create") ? "visible" : "excluded"); - this._add(newWalletButton); - */ - this.loadWallets(); }, @@ -109,25 +102,6 @@ qx.Class.define("osparc.desktop.wallets.WalletsList", { return wallet; }, - __getCreateWalletSection: function() { - const createWalletBtn = new qx.ui.form.Button().set({ - appearance: "strong-button", - label: this.tr("New Credit Account"), - alignX: "center", - icon: "@FontAwesome5Solid/plus/14", - allowGrowX: false - }); - createWalletBtn.addListener("execute", function() { - const newWallet = true; - const walletEditor = new osparc.desktop.wallets.WalletEditor(newWallet); - const title = this.tr("Credit Account Details Editor"); - const win = osparc.ui.window.Window.popUpInWindow(walletEditor, title, 400, 250); - walletEditor.addListener("createWallet", () => this.__createWallet(win, walletEditor.getChildControl("create"), walletEditor)); - walletEditor.addListener("cancel", () => win.close()); - }, this); - return createWalletBtn; - }, - __getWalletsFilter: function() { const filter = new osparc.filter.TextFilter("text", "walletsList").set({ allowStretchX: true, @@ -211,8 +185,7 @@ qx.Class.define("osparc.desktop.wallets.WalletsList", { return; } - const newWallet = false; - const walletEditor = new osparc.desktop.wallets.WalletEditor(newWallet); + const walletEditor = new osparc.desktop.wallets.WalletEditor(); wallet.bind("walletId", walletEditor, "walletId"); wallet.bind("name", walletEditor, "name"); wallet.bind("description", walletEditor, "description"); @@ -225,38 +198,6 @@ qx.Class.define("osparc.desktop.wallets.WalletsList", { walletEditor.addListener("cancel", () => win.close()); }, - __createWallet: function(win, button, walletEditor) { - button.setFetching(true); - - const name = walletEditor.getName(); - const description = walletEditor.getDescription(); - const thumbnail = walletEditor.getThumbnail(); - - const params = { - data: { - "name": name, - "description": description || null, - "thumbnail": thumbnail || null - } - }; - osparc.data.Resources.fetch("wallets", "post", params) - .then(() => { - const store = osparc.store.Store.getInstance(); - osparc.store.Store.getInstance().invalidate("wallets"); - store.reloadWallets() - .then(() => this.loadWallets()); - }) - .catch(err => { - console.error(err); - const msg = err.message || this.tr("Something went wrong creating the Wallet"); - osparc.FlashMessenger.getInstance().logAs(msg, "ERROR"); - }) - .finally(() => { - button.setFetching(false); - win.close(); - }); - }, - __updateWallet: function(win, button, walletEditor) { button.setFetching(true); @@ -280,9 +221,9 @@ qx.Class.define("osparc.desktop.wallets.WalletsList", { }; osparc.data.Resources.fetch("wallets", "put", params) .then(() => { - osparc.store.Store.getInstance().invalidate("wallets"); - store.reloadWallets() - .then(() => this.loadWallets()); + osparc.FlashMessenger.getInstance().logAs(name + this.tr(" successfully edited")); + const wallet = osparc.desktop.credits.Utils.getWallet(walletId); + wallet.set(params.data); }) .catch(err => { console.error(err); diff --git a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js index ac2f89f42e3..3631ca02d73 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js +++ b/services/static-webserver/client/source/class/osparc/navigation/NavigationBar.js @@ -180,8 +180,8 @@ qx.Class.define("osparc.navigation.NavigationBar", { case "logo-powered": control = new osparc.ui.basic.PoweredByOsparc().set({ padding: 3, - paddingTop: 1, - maxHeight: this.self().HEIGHT - 2 + paddingTop: 2, + maxHeight: this.self().HEIGHT - 5 }); this.getChildControl("left-items").add(control); break; diff --git a/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js b/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js index 944ea5855f5..e4be559d903 100644 --- a/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js +++ b/services/static-webserver/client/source/class/osparc/navigation/UserMenuButton.js @@ -100,10 +100,7 @@ qx.Class.define("osparc.navigation.UserMenuButton", { break; case "po-center": control = new qx.ui.menu.Button(this.tr("PO Center")); - control.addListener("execute", () => { - const poCenterWindow = osparc.po.POCenterWindow.openWindow(); - poCenterWindow.openInvitations(); - }, this); + control.addListener("execute", () => osparc.po.POCenterWindow.openWindow(), this); this.getMenu().add(control); break; case "preferences": diff --git a/services/static-webserver/client/source/class/osparc/node/UpdateResourceLimitsView.js b/services/static-webserver/client/source/class/osparc/node/UpdateResourceLimitsView.js index e4d9f83c04b..d17108534ab 100644 --- a/services/static-webserver/client/source/class/osparc/node/UpdateResourceLimitsView.js +++ b/services/static-webserver/client/source/class/osparc/node/UpdateResourceLimitsView.js @@ -100,7 +100,7 @@ qx.Class.define("osparc.node.UpdateResourceLimitsView", { if (resourceKey === "RAM") { value = osparc.utils.Utils.bytesToGB(value); } - const spinner = new qx.ui.form.Spinner(0, value, 200).set({ + const spinner = new qx.ui.form.Spinner(0, value, 512).set({ singleStep: 0.1 }); const nf = new qx.util.format.NumberFormat(); diff --git a/services/static-webserver/client/source/class/osparc/po/Invitations.js b/services/static-webserver/client/source/class/osparc/po/Invitations.js index 87e54f65668..28d59bf7010 100644 --- a/services/static-webserver/client/source/class/osparc/po/Invitations.js +++ b/services/static-webserver/client/source/class/osparc/po/Invitations.js @@ -104,7 +104,7 @@ qx.Class.define("osparc.po.Invitations", { maximum: 1000, value: 0 }); - form.add(extraCreditsInUsd, this.tr("Welcome Credits (US $)")); + form.add(extraCreditsInUsd, this.tr("Welcome Credits (US$)")); const withExpiration = new qx.ui.form.CheckBox().set({ value: false diff --git a/services/static-webserver/client/source/class/osparc/po/POCenter.js b/services/static-webserver/client/source/class/osparc/po/POCenter.js index 37fa5ec69b0..9619b506847 100644 --- a/services/static-webserver/client/source/class/osparc/po/POCenter.js +++ b/services/static-webserver/client/source/class/osparc/po/POCenter.js @@ -27,45 +27,46 @@ qx.Class.define("osparc.po.POCenter", { padding: 10 }); - const tabViews = this.__tabsView = new qx.ui.tabview.TabView().set({ + const tabViews = new qx.ui.tabview.TabView().set({ barPosition: "left", contentPadding: 0 }); tabViews.getChildControl("bar").add(osparc.desktop.credits.UserCenter.createMiniProfileView()); - const invitationsPage = this.__invitationsPage = this.__getInvitationsPage(); + const invitationsPage = this.__getInvitationsPage(); tabViews.add(invitationsPage); + const productPage = this.__getProductPage(); + tabViews.add(productPage); + this._add(tabViews); }, members: { - __tabsView: null, - __invitationsPage: null, - __getInvitationsPage: function() { const title = this.tr("Invitations"); const iconSrc = "@FontAwesome5Solid/envelope/22"; const page = new osparc.desktop.preferences.pages.BasePage(title, iconSrc); page.showLabelOnTab(); - const overview = new osparc.po.Invitations(); - overview.set({ + const invitations = new osparc.po.Invitations(); + invitations.set({ margin: 10 }); - page.add(overview); + page.add(invitations); return page; }, - __openPage: function(page) { - if (page) { - this.__tabsView.setSelection([page]); - } - }, - - openInvitations: function() { - if (this.__invitationsPage) { - this.__openPage(this.__invitationsPage); - } + __getProductPage: function() { + const title = this.tr("Product Info"); + const iconSrc = "@FontAwesome5Solid/info/22"; + const page = new osparc.desktop.preferences.pages.BasePage(title, iconSrc); + page.showLabelOnTab(); + const productInfo = new osparc.po.ProductInfo(); + productInfo.set({ + margin: 10 + }); + page.add(productInfo); + return page; } } }); diff --git a/services/static-webserver/client/source/class/osparc/po/POCenterWindow.js b/services/static-webserver/client/source/class/osparc/po/POCenterWindow.js index cc0e6cb0307..3302626089a 100644 --- a/services/static-webserver/client/source/class/osparc/po/POCenterWindow.js +++ b/services/static-webserver/client/source/class/osparc/po/POCenterWindow.js @@ -35,7 +35,7 @@ qx.Class.define("osparc.po.POCenterWindow", { appearance: "service-window" }); - const poCenter = this.__poCenter = new osparc.po.POCenter(); + const poCenter = new osparc.po.POCenter(); this.add(poCenter); }, @@ -46,13 +46,5 @@ qx.Class.define("osparc.po.POCenterWindow", { accountWindow.open(); return accountWindow; } - }, - - members: { - __poCenter: null, - - openInvitations: function() { - this.__poCenter.openInvitations(); - } } }); diff --git a/services/static-webserver/client/source/class/osparc/po/ProductInfo.js b/services/static-webserver/client/source/class/osparc/po/ProductInfo.js new file mode 100644 index 00000000000..b84be372555 --- /dev/null +++ b/services/static-webserver/client/source/class/osparc/po/ProductInfo.js @@ -0,0 +1,49 @@ +/* ************************************************************************ + + osparc - the simcore frontend + + https://osparc.io + + Copyright: + 2023 IT'IS Foundation, https://itis.swiss + + License: + MIT: https://opensource.org/licenses/MIT + + Authors: + * Odei Maiz (odeimaiz) + +************************************************************************ */ + +qx.Class.define("osparc.po.ProductInfo", { + extend: qx.ui.core.Widget, + + construct: function() { + this.base(arguments); + + this._setLayout(new qx.ui.layout.VBox(10)); + + this.__fetchInfo(); + }, + + members: { + __fetchInfo: function() { + const params = { + url: { + productName: osparc.product.Utils.getProductName() + } + }; + osparc.data.Resources.fetch("productMetadata", "get", params) + .then(respData => { + const invitationRespViewer = new osparc.ui.basic.JsonTreeWidget(respData, "product-metadata"); + const container = new qx.ui.container.Scroll().set({ + maxHeight: 500 + }); + container.add(invitationRespViewer); + this._add(container, { + flex: 1 + }); + }); + } + } +}); diff --git a/services/static-webserver/client/source/class/osparc/product/Utils.js b/services/static-webserver/client/source/class/osparc/product/Utils.js index 94d24468a2e..062bdfe8616 100644 --- a/services/static-webserver/client/source/class/osparc/product/Utils.js +++ b/services/static-webserver/client/source/class/osparc/product/Utils.js @@ -129,7 +129,7 @@ qx.Class.define("osparc.product.Utils", { const product = qx.core.Environment.get("product.name"); switch (product) { case "s4l": - logosPath = lightLogo ? "osparc/s4l_logo_short_white.svg" : "osparc/s4l_logo_short_black.svg"; + logosPath = lightLogo ? "osparc/s4l_logo_white_short.svg" : "osparc/s4l_logo_black_short.svg"; break; case "s4llite": logosPath = lightLogo ? "osparc/s4llite-white.png" : "osparc/s4llite-black.png"; diff --git a/services/static-webserver/client/source/class/osparc/resourceUsage/OverviewTable.js b/services/static-webserver/client/source/class/osparc/resourceUsage/OverviewTable.js deleted file mode 100644 index c5e317b7043..00000000000 --- a/services/static-webserver/client/source/class/osparc/resourceUsage/OverviewTable.js +++ /dev/null @@ -1,135 +0,0 @@ -/* ************************************************************************ - - osparc - the simcore frontend - - https://osparc.io - - Copyright: - 2023 IT'IS Foundation, https://itis.swiss - - License: - MIT: https://opensource.org/licenses/MIT - - Authors: - * Odei Maiz (odeimaiz) - -************************************************************************ */ - -qx.Class.define("osparc.resourceUsage.OverviewTable", { - extend: osparc.ui.table.Table, - - construct: function() { - const model = this.__model = new qx.ui.table.model.Simple(); - const cols = this.self().COLUMNS; - const colNames = []; - Object.entries(cols).forEach(([key, data]) => { - if (["wallet", "user"].includes(key) && !osparc.desktop.credits.Utils.areWalletsEnabled() - ) { - return; - } - colNames.push(data.title); - }); - model.setColumns(colNames); - - this.base(arguments, model, { - tableColumnModel: obj => new qx.ui.table.columnmodel.Resize(obj), - statusBarVisible: false - }); - this.makeItLoose(); - - const columnModel = this.getTableColumnModel(); - columnModel.getBehavior().setWidth(this.self().COLUMNS.duration.pos, 70); - columnModel.getBehavior().setWidth(this.self().COLUMNS.status.pos, 70); - if (osparc.desktop.credits.Utils.areWalletsEnabled()) { - columnModel.getBehavior().setWidth(this.self().COLUMNS.wallet.pos, 100); - } - columnModel.getBehavior().setWidth(this.self().COLUMNS.cost.pos, 60); - }, - - statics: { - COLUMNS: { - project: { - pos: 0, - title: osparc.product.Utils.getStudyAlias({firstUpperCase: true}) - }, - node: { - pos: 1, - title: qx.locale.Manager.tr("Node") - }, - service: { - pos: 2, - title: qx.locale.Manager.tr("Service") - }, - start: { - pos: 3, - title: qx.locale.Manager.tr("Start") - }, - duration: { - pos: 4, - title: qx.locale.Manager.tr("Duration") - }, - status: { - pos: 5, - title: qx.locale.Manager.tr("Status") - }, - wallet: { - pos: 6, - title: qx.locale.Manager.tr("Credit Account") - }, - cost: { - pos: 7, - title: qx.locale.Manager.tr("Cost") - }, - user: { - pos: 8, - title: qx.locale.Manager.tr("User") - } - }, - - respDataToTableData: async function(datas) { - const newDatas = []; - if (datas) { - const cols = this.COLUMNS; - for (const data of datas) { - const newData = []; - newData[cols["project"].pos] = data["project_name"] ? data["project_name"] : data["project_id"]; - newData[cols["node"].pos] = data["node_name"] ? data["node_name"] : data["node_id"]; - if (data["service_key"]) { - const parts = data["service_key"].split("/"); - const serviceName = parts.pop(); - newData[cols["service"].pos] = serviceName + ":" + data["service_version"]; - } - if (data["started_at"]) { - const startTime = new Date(data["started_at"]); - newData[cols["start"].pos] = osparc.utils.Utils.formatDateAndTime(startTime); - if (data["stopped_at"]) { - const stopTime = new Date(data["stopped_at"]); - const durationTime = stopTime - startTime; - newData[cols["duration"].pos] = osparc.utils.Utils.formatMilliSeconds(durationTime); - } - } - newData[cols["status"].pos] = qx.lang.String.firstUp(data["service_run_status"].toLowerCase()); - if (osparc.desktop.credits.Utils.areWalletsEnabled()) { - newData[cols["wallet"].pos] = data["wallet_name"] ? data["wallet_name"] : "-"; - } - newData[cols["cost"].pos] = data["credit_cost"] ? data["credit_cost"] : "-"; - if (osparc.desktop.credits.Utils.areWalletsEnabled()) { - const user = await osparc.store.Store.getInstance().getUser(data["user_id"]); - newData[cols["user"].pos] = user ? user["label"] : data["user_id"]; - } - newDatas.push(newData); - } - } - return newDatas; - } - }, - - members: { - __model: null, - - addData: async function(datas) { - const newDatas = await this.self().respDataToTableData(datas); - this.setData(newDatas); - } - } -}); diff --git a/services/static-webserver/client/source/class/osparc/store/Store.js b/services/static-webserver/client/source/class/osparc/store/Store.js index 36efd809eb9..8b04e221502 100644 --- a/services/static-webserver/client/source/class/osparc/store/Store.js +++ b/services/static-webserver/client/source/class/osparc/store/Store.js @@ -129,6 +129,11 @@ qx.Class.define("osparc.store.Store", { nullable: true, event: "changeCreditPrice" }, + productMetadata: { + check: "Object", + init: {}, + nullable: true + }, permissions: { check: "Array", init: [] diff --git a/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js b/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js index 61e26e4a33e..24453fe0bf8 100644 --- a/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js +++ b/services/static-webserver/client/source/class/osparc/ui/basic/JsonTreeWidget.js @@ -39,7 +39,8 @@ qx.Class.define("osparc.ui.basic.JsonTreeWidget", { const prettyJson = JSON.stringify(data, null, " ").replace(/\n/ig, "
"); this.base(arguments, prettyJson); this.set({ - rich: true + rich: true, + selectable: true }); } }); diff --git a/services/static-webserver/client/source/class/osparc/ui/basic/PoweredByOsparc.js b/services/static-webserver/client/source/class/osparc/ui/basic/PoweredByOsparc.js index 3dbb4b4fcec..bee9711f0d2 100644 --- a/services/static-webserver/client/source/class/osparc/ui/basic/PoweredByOsparc.js +++ b/services/static-webserver/client/source/class/osparc/ui/basic/PoweredByOsparc.js @@ -57,8 +57,8 @@ qx.Class.define("osparc.ui.basic.PoweredByOsparc", { break; case "logo": { control = new qx.ui.basic.Image().set({ - width: 42, - height: 33, + width: 32, + height: 24, scale: true }); const logoContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox().set({ diff --git a/services/static-webserver/client/source/class/osparc/utils/LibVersions.js b/services/static-webserver/client/source/class/osparc/utils/LibVersions.js index defb19d5feb..f560acbf1fb 100644 --- a/services/static-webserver/client/source/class/osparc/utils/LibVersions.js +++ b/services/static-webserver/client/source/class/osparc/utils/LibVersions.js @@ -67,7 +67,7 @@ qx.Class.define("osparc.utils.LibVersions", { return { name: name, - version: commitId.substring(0, 7), + version: commitId ? commitId.substring(0, 7) : "", url: remoteUrl }; }, diff --git a/services/static-webserver/client/source/resource/osparc/s4l_logo_short_black.svg b/services/static-webserver/client/source/resource/osparc/s4l_logo_black_short.svg similarity index 100% rename from services/static-webserver/client/source/resource/osparc/s4l_logo_short_black.svg rename to services/static-webserver/client/source/resource/osparc/s4l_logo_black_short.svg diff --git a/services/static-webserver/client/source/resource/osparc/s4l_logo_short_white.svg b/services/static-webserver/client/source/resource/osparc/s4l_logo_white_short.svg similarity index 100% rename from services/static-webserver/client/source/resource/osparc/s4l_logo_short_white.svg rename to services/static-webserver/client/source/resource/osparc/s4l_logo_white_short.svg diff --git a/services/web/server/src/simcore_service_webserver/projects/db.py b/services/web/server/src/simcore_service_webserver/projects/db.py index d700f69d5db..45e1c1f0d54 100644 --- a/services/web/server/src/simcore_service_webserver/projects/db.py +++ b/services/web/server/src/simcore_service_webserver/projects/db.py @@ -31,9 +31,6 @@ from servicelib.aiohttp.application_keys import APP_DB_ENGINE_KEY from servicelib.logging_utils import get_log_record_extra, log_context from simcore_postgres_database.errors import UniqueViolation -from simcore_postgres_database.models.projects_node_to_pricing_unit import ( - projects_node_to_pricing_unit, -) from simcore_postgres_database.models.projects_nodes import projects_nodes from simcore_postgres_database.models.projects_to_products import projects_to_products from simcore_postgres_database.models.wallets import wallets @@ -807,27 +804,14 @@ async def get_project_node_pricing_unit_id( project_uuid: ProjectID, node_uuid: NodeID, ) -> PricingPlanAndUnitIdsTuple | None: + project_nodes_repo = ProjectNodesRepo(project_uuid=project_uuid) async with self.engine.acquire() as conn: - result = await conn.execute( - sa.select( - projects_node_to_pricing_unit.c.pricing_plan_id, - projects_node_to_pricing_unit.c.pricing_unit_id, - ) - .select_from( - projects_nodes.join( - projects_node_to_pricing_unit, - projects_nodes.c.project_node_id - == projects_node_to_pricing_unit.c.project_node_id, - ) - ) - .where( - (projects_nodes.c.project_uuid == f"{project_uuid}") - & (projects_nodes.c.node_id == f"{node_uuid}") - ) + output = await project_nodes_repo.get_project_node_pricing_unit_id( + conn, node_uuid=node_uuid ) - row = await result.fetchone() - if row: - return PricingPlanAndUnitIdsTuple(row[0], row[1]) + if output: + pricing_plan_id, pricing_unit_id = output + return PricingPlanAndUnitIdsTuple(pricing_plan_id, pricing_unit_id) return None async def connect_pricing_unit_to_project_node( @@ -838,32 +822,13 @@ async def connect_pricing_unit_to_project_node( pricing_unit_id: PricingUnitId, ) -> None: async with self.engine.acquire() as conn: - result = await conn.scalar( - sa.select(projects_nodes.c.project_node_id).where( - (projects_nodes.c.project_uuid == f"{project_uuid}") - & (projects_nodes.c.node_id == f"{node_uuid}") - ) - ) - project_node_id = parse_obj_as(int, result) if result else 0 - - insert_stmt = pg_insert(projects_node_to_pricing_unit).values( - project_node_id=project_node_id, + project_nodes_repo = ProjectNodesRepo(project_uuid=project_uuid) + await project_nodes_repo.connect_pricing_unit_to_project_node( + conn, + node_uuid=node_uuid, pricing_plan_id=pricing_plan_id, pricing_unit_id=pricing_unit_id, - created=sa.func.now(), - modified=sa.func.now(), ) - on_update_stmt = insert_stmt.on_conflict_do_update( - index_elements=[ - projects_node_to_pricing_unit.c.project_node_id, - ], - set_={ - "pricing_plan_id": insert_stmt.excluded.pricing_plan_id, - "pricing_unit_id": insert_stmt.excluded.pricing_unit_id, - "modified": sa.func.now(), - }, - ) - await conn.execute(on_update_stmt) # # Project ACCESS RIGHTS/PERMISSIONS