From 9c91c1dd979aa543bbcdabf1f54e9af1a6fe902a Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Fri, 11 Oct 2024 17:58:31 +0200 Subject: [PATCH] feat: Add support for provisioning of tool models The already known provisioning feature for read-only sessions will be extended to persistent workspace sessions. Per session, a user can request a persistent workspace session and can pass optionally one model to provision. It's not possible to provision more than one model (aka one Git repository). The provisioning takes place once. We'll not touch the provisioned workspace until the user explicently resets the provisioning. The provisioning revision and date are stored in the database. When the users reset their workspace, we'll remove the provisioning model from the database. During the next session start, the matching workspace will be re-initialized and a copy will be saved to a `.bak` directory. This feature is essential for the start-up of trainings. --- .github/workflows/openapi.yml | 2 +- .pre-commit-config.yaml | 9 +- .../014438261702_add_provisioning_feature.py | 51 + .../2f8449c217fa_add_project_tools_table.py | 36 + ...add_environment_and_connection_info_to_.py | 4 +- .../capellacollab/core/database/migration.py | 4 +- backend/capellacollab/core/database/models.py | 2 + backend/capellacollab/core/responses.py | 30 + backend/capellacollab/projects/models.py | 6 + backend/capellacollab/projects/routes.py | 6 + .../toolmodels/backups/runs/interface.py | 2 + .../capellacollab/projects/toolmodels/crud.py | 8 +- .../toolmodels/modelbadge/exceptions.py | 2 +- .../projects/toolmodels/models.py | 16 + .../toolmodels/modelsources/git/routes.py | 6 +- .../toolmodels/provisioning/__init__.py | 2 + .../projects/toolmodels/provisioning/crud.py | 37 + .../toolmodels/provisioning/exceptions.py | 16 + .../toolmodels/provisioning/injectables.py | 26 + .../toolmodels/provisioning/models.py | 66 + .../toolmodels/provisioning/routes.py | 60 + .../projects/toolmodels/readme/routes.py | 40 + .../projects/toolmodels/routes.py | 12 + .../capellacollab/projects/tools/__init__.py | 2 + backend/capellacollab/projects/tools/crud.py | 56 + .../projects/tools/exceptions.py | 36 + .../projects/tools/injectables.py | 28 + .../capellacollab/projects/tools/models.py | 64 + .../capellacollab/projects/tools/routes.py | 117 + backend/capellacollab/sessions/exceptions.py | 36 + .../capellacollab/sessions/hooks/__init__.py | 2 + .../sessions/hooks/authentication.py | 8 +- .../capellacollab/sessions/hooks/guacamole.py | 39 +- backend/capellacollab/sessions/hooks/http.py | 23 +- .../capellacollab/sessions/hooks/interface.py | 232 +- .../capellacollab/sessions/hooks/jupyter.py | 11 +- .../sessions/hooks/networking.py | 29 +- .../sessions/hooks/persistent_workspace.py | 24 +- .../sessions/hooks/project_scope.py | 44 + .../sessions/hooks/provisioning.py | 246 +- .../sessions/hooks/pure_variants.py | 19 +- .../sessions/hooks/read_only_workspace.py | 8 +- .../sessions/hooks/session_preparation.py | 19 +- backend/capellacollab/sessions/hooks/t4c.py | 41 +- backend/capellacollab/sessions/idletimeout.py | 2 + backend/capellacollab/sessions/injection.py | 27 +- backend/capellacollab/sessions/models.py | 33 +- .../capellacollab/sessions/operators/k8s.py | 7 +- backend/capellacollab/sessions/routes.py | 92 +- backend/capellacollab/sessions/util.py | 35 +- .../settings/modelsources/git/core.py | 39 +- .../settings/modelsources/git/exceptions.py | 10 + .../settings/modelsources/git/routes.py | 4 +- backend/capellacollab/tools/models.py | 7 + backend/pyproject.toml | 1 + backend/tests/core/test_auth_injectables.py | 7 - backend/tests/projects/test_projects_tools.py | 119 + .../projects/test_projects_users_routes.py | 48 +- .../toolmodels/provisioning/fixtures.py | 35 + .../provisioning/test_provisioning.py | 88 + backend/tests/sessions/hooks/conftest.py | 87 + .../sessions/hooks/test_guacamole_hook.py | 63 +- .../tests/sessions/hooks/test_http_hook.py | 32 +- .../tests/sessions/hooks/test_jupyter_hook.py | 34 +- .../sessions/hooks/test_networking_hook.py | 17 +- .../hooks/test_persistent_workspace.py | 51 +- .../hooks/test_pre_authentiation_hook.py | 13 +- .../sessions/hooks/test_project_scope.py | 23 + .../sessions/hooks/test_provisioning_hook.py | 375 ++- .../sessions/hooks/test_pure_variants.py | 22 +- .../hooks/test_session_preparation.py | 21 +- backend/tests/sessions/hooks/test_t4c_hook.py | 96 +- .../sessions/test_session_environment.py | 19 +- backend/tests/sessions/test_session_hooks.py | 54 +- backend/tests/sessions/test_session_routes.py | 59 + backend/tests/settings/fixtures.py | 33 + backend/tests/settings/test_git_instances.py | 22 +- backend/tests/test_event_creation.py | 117 +- backend/tests/users/fixtures.py | 12 +- docs/docs/admin/tools/configuration.md | 7 + docs/docs/user/sessions/types/index.md | 53 +- frontend/.prettierrc.js | 2 + frontend/package-lock.json | 2083 ++++++++++++++--- frontend/package.json | 3 + frontend/src/app/app-routing.module.ts | 11 + .../app/general/footer/footer.component.html | 3 +- .../src/app/openapi/.openapi-generator/FILES | 7 + frontend/src/app/openapi/api/api.ts | 8 +- .../projects-models-provisioning.service.ts | 248 ++ .../api/projects-models-readme.service.ts | 171 ++ .../app/openapi/api/projects-tools.service.ts | 330 +++ .../app/openapi/model/model-provisioning.ts | 21 + frontend/src/app/openapi/model/models.ts | 4 + .../model/post-project-tool-request.ts | 18 + .../app/openapi/model/post-session-request.ts | 6 +- .../src/app/openapi/model/project-tool.ts | 23 + .../model/session-provisioning-request.ts | 2 +- .../simple-tool-model-without-project.ts | 21 + .../model/tool-model-provisioning-input.ts | 4 + .../model/tool-model-provisioning-output.ts | 4 + .../create-model/create-model.component.css | 4 - .../create-model/create-model.component.ts | 1 - .../model-diagram-dialog.component.html | 3 +- .../projects/models/service/model.service.ts | 5 +- .../create-project-tools.component.html | 56 + .../create-project-tools.component.ts | 111 + .../model-complexity-badge.component.html | 6 +- .../model-overview.component.css | 8 - .../model-overview.component.html | 373 +-- .../model-overview.component.ts | 1 - .../project-details.component.html | 17 +- .../project-details.component.ts | 20 +- .../project-detail/project-details.stories.ts | 35 + .../project-tools-wrapper.service.ts | 42 + .../project-tools.component.html | 104 + .../project-tools/project-tools.component.ts | 69 + .../project-tools/project-tools.stories.ts | 99 + .../training-details.component.html | 119 + .../training-details.component.ts | 92 + .../training-details.stories.ts | 91 + .../project-overview.component.html | 34 +- .../feedback-dialog.component.html | 16 +- .../feedback-dialog.component.ts | 3 +- .../floating-window-manager.component.html | 3 +- .../src/app/sessions/sessions.component.html | 9 +- .../active-sessions.component.html | 2 +- .../active-sessions.stories.ts | 2 +- .../connection-dialog.stories.ts | 4 +- .../create-readonly-session.component.html | 44 +- .../create-readonly-session.component.ts | 12 +- .../create-readonly-session.stories.ts | 27 + .../create-readonly-session.component.css | 12 - .../create-persistent-session.component.html | 40 +- .../create-persistent-session.component.ts | 8 +- .../create-persistent-session.stories.ts | 6 +- .../create-provisioned-session.component.html | 144 ++ ...e-provisioned-session.component.stories.ts | 131 ++ .../create-provisioned-session.component.ts | 224 ++ .../create-readonly-session-dialog.stories.ts | 12 +- .../create-session-history.component.html | 3 +- .../create-session-history.stories.ts | 12 +- .../edit-t4c-instance.stories.ts | 4 +- .../user-settings.component.html | 2 +- frontend/src/storybook/git.ts | 6 +- frontend/src/storybook/model.ts | 44 +- frontend/src/storybook/project-tools.ts | 48 + frontend/src/storybook/project-users.ts | 11 + frontend/src/storybook/project.ts | 10 + frontend/src/storybook/t4c.ts | 4 +- frontend/src/storybook/tool.ts | 120 +- frontend/src/storybook/user.ts | 2 +- .../session-preparation/clone_repositories.py | 42 +- 152 files changed, 7024 insertions(+), 1533 deletions(-) create mode 100644 backend/capellacollab/alembic/versions/014438261702_add_provisioning_feature.py create mode 100644 backend/capellacollab/alembic/versions/2f8449c217fa_add_project_tools_table.py create mode 100644 backend/capellacollab/projects/toolmodels/provisioning/__init__.py create mode 100644 backend/capellacollab/projects/toolmodels/provisioning/crud.py create mode 100644 backend/capellacollab/projects/toolmodels/provisioning/exceptions.py create mode 100644 backend/capellacollab/projects/toolmodels/provisioning/injectables.py create mode 100644 backend/capellacollab/projects/toolmodels/provisioning/models.py create mode 100644 backend/capellacollab/projects/toolmodels/provisioning/routes.py create mode 100644 backend/capellacollab/projects/toolmodels/readme/routes.py create mode 100644 backend/capellacollab/projects/tools/__init__.py create mode 100644 backend/capellacollab/projects/tools/crud.py create mode 100644 backend/capellacollab/projects/tools/exceptions.py create mode 100644 backend/capellacollab/projects/tools/injectables.py create mode 100644 backend/capellacollab/projects/tools/models.py create mode 100644 backend/capellacollab/projects/tools/routes.py create mode 100644 backend/capellacollab/sessions/hooks/project_scope.py create mode 100644 backend/tests/projects/test_projects_tools.py create mode 100644 backend/tests/projects/toolmodels/provisioning/fixtures.py create mode 100644 backend/tests/projects/toolmodels/provisioning/test_provisioning.py create mode 100644 backend/tests/sessions/hooks/conftest.py create mode 100644 backend/tests/sessions/hooks/test_project_scope.py create mode 100644 backend/tests/settings/fixtures.py create mode 100644 frontend/src/app/openapi/api/projects-models-provisioning.service.ts create mode 100644 frontend/src/app/openapi/api/projects-models-readme.service.ts create mode 100644 frontend/src/app/openapi/api/projects-tools.service.ts create mode 100644 frontend/src/app/openapi/model/model-provisioning.ts create mode 100644 frontend/src/app/openapi/model/post-project-tool-request.ts create mode 100644 frontend/src/app/openapi/model/project-tool.ts create mode 100644 frontend/src/app/openapi/model/simple-tool-model-without-project.ts delete mode 100644 frontend/src/app/projects/models/create-model/create-model.component.css create mode 100644 frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.html create mode 100644 frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.ts delete mode 100644 frontend/src/app/projects/project-detail/model-overview/model-overview.component.css create mode 100644 frontend/src/app/projects/project-detail/project-tools/project-tools-wrapper.service.ts create mode 100644 frontend/src/app/projects/project-detail/project-tools/project-tools.component.html create mode 100644 frontend/src/app/projects/project-detail/project-tools/project-tools.component.ts create mode 100644 frontend/src/app/projects/project-detail/project-tools/project-tools.stories.ts create mode 100644 frontend/src/app/projects/project-detail/training-details/training-details.component.html create mode 100644 frontend/src/app/projects/project-detail/training-details/training-details.component.ts create mode 100644 frontend/src/app/projects/project-detail/training-details/training-details.stories.ts rename frontend/src/app/sessions/user-sessions-wrapper/{create-session => }/create-readonly-session/create-readonly-session.component.html (71%) rename frontend/src/app/sessions/user-sessions-wrapper/{create-session => }/create-readonly-session/create-readonly-session.component.ts (93%) create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.stories.ts delete mode 100644 frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.css create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.html create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.stories.ts create mode 100644 frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.ts create mode 100644 frontend/src/storybook/project-tools.ts diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index e81d84a2c4..9c8e5df254 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -87,7 +87,7 @@ jobs: docker run --rm -v /tmp:/tmp:ro tufin/oasdiff changelog \ --format markup \ /tmp/openapi.json /tmp/openapi2.json \ - | sed 's/\/anyOf\[subschema #[0-9]\+\(: [a-zA-Z]\+\)\?\]//g' + | sed 's/#\([0-9]\+\)/\1/g' echo 'EOF' } >> "$GITHUB_OUTPUT" - name: Find existing comment on PR diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77f6dba1a8..c24d8a7d7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -83,9 +83,14 @@ repos: - id: pylint name: pylint entry: pylint - args: [--rcfile=./backend/pyproject.toml] + args: [ + '-rn', # Only display messages + '-sn', # Don't display the score + '--rcfile=./backend/pyproject.toml', + ] language: system types: [python] + require_serial: true files: '^backend' exclude: '^backend/capellacollab/alembic/' - repo: local @@ -101,6 +106,8 @@ repos: - 'prettier-plugin-tailwindcss@^0.6.8' - '@trivago/prettier-plugin-sort-imports@^4.3.0' - 'tailwindcss@^3.4.12' + - 'prettier-plugin-classnames@^0.7.4' + - 'prettier-plugin-merge@^0.7.1' - repo: https://github.com/Lucas-C/pre-commit-hooks rev: v1.5.5 hooks: diff --git a/backend/capellacollab/alembic/versions/014438261702_add_provisioning_feature.py b/backend/capellacollab/alembic/versions/014438261702_add_provisioning_feature.py new file mode 100644 index 0000000000..df65ff933b --- /dev/null +++ b/backend/capellacollab/alembic/versions/014438261702_add_provisioning_feature.py @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Add provisioning feature + +Revision ID: 014438261702 +Revises: 3818a5009130 +Create Date: 2024-10-11 17:34:05.210906 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "014438261702" +down_revision = "3818a5009130" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "model_provisioning", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("user_id", sa.Integer(), nullable=False), + sa.Column("tool_model_id", sa.Integer(), nullable=False), + sa.Column("revision", sa.String(), nullable=False), + sa.Column("commit_hash", sa.String(), nullable=False), + sa.Column("provisioned_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint( + ["tool_model_id"], + ["models.id"], + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index( + op.f("ix_model_provisioning_id"), + "model_provisioning", + ["id"], + unique=False, + ) + op.add_column( + "sessions", sa.Column("provisioning_id", sa.Integer(), nullable=True) + ) + op.create_foreign_key( + None, "sessions", "model_provisioning", ["provisioning_id"], ["id"] + ) diff --git a/backend/capellacollab/alembic/versions/2f8449c217fa_add_project_tools_table.py b/backend/capellacollab/alembic/versions/2f8449c217fa_add_project_tools_table.py new file mode 100644 index 0000000000..22e28eff5b --- /dev/null +++ b/backend/capellacollab/alembic/versions/2f8449c217fa_add_project_tools_table.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Add project tools table + +Revision ID: 2f8449c217fa +Revises: 014438261702 +Create Date: 2024-10-29 14:11:47.774679 + +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "2f8449c217fa" +down_revision = "014438261702" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "project_tool_association", + sa.Column("id", sa.Integer(), autoincrement=True, nullable=False), + sa.Column("project_id", sa.Integer(), nullable=False), + sa.Column("tool_version_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["project_id"], + ["projects.id"], + ), + sa.ForeignKeyConstraint( + ["tool_version_id"], + ["versions.id"], + ), + sa.PrimaryKeyConstraint("id", "project_id", "tool_version_id"), + ) diff --git a/backend/capellacollab/alembic/versions/7683b08b00ba_add_environment_and_connection_info_to_.py b/backend/capellacollab/alembic/versions/7683b08b00ba_add_environment_and_connection_info_to_.py index 51ab303d05..61dd9b2be6 100644 --- a/backend/capellacollab/alembic/versions/7683b08b00ba_add_environment_and_connection_info_to_.py +++ b/backend/capellacollab/alembic/versions/7683b08b00ba_add_environment_and_connection_info_to_.py @@ -134,9 +134,7 @@ def get_eclipse_configuration(): "{CAPELLACOLLAB_SESSIONS_SCHEME}://{CAPELLACOLLAB_SESSIONS_HOST}:{CAPELLACOLLAB_SESSIONS_PORT}" "{CAPELLACOLLAB_SESSIONS_BASE_PATH}/?floating_menu=0&path={CAPELLACOLLAB_SESSIONS_BASE_PATH}/" ), - "cookies": { - "token": "{CAPELLACOLLAB_SESSION_TOKEN}", - }, + "cookies": {}, }, ] }, diff --git a/backend/capellacollab/core/database/migration.py b/backend/capellacollab/core/database/migration.py index 8b7875918f..d659378027 100644 --- a/backend/capellacollab/core/database/migration.py +++ b/backend/capellacollab/core/database/migration.py @@ -166,9 +166,7 @@ def get_eclipse_session_configuration() -> ( ) + "{CAPELLACOLLAB_SESSIONS_BASE_PATH}/?floating_menu=0&path={CAPELLACOLLAB_SESSIONS_BASE_PATH}/" ), - cookies={ - "token": "{CAPELLACOLLAB_SESSION_TOKEN}", - }, + cookies={}, sharing=tools_models.ToolSessionSharingConfiguration( enabled=True ), diff --git a/backend/capellacollab/core/database/models.py b/backend/capellacollab/core/database/models.py index c82af1b56d..8f2ac6e160 100644 --- a/backend/capellacollab/core/database/models.py +++ b/backend/capellacollab/core/database/models.py @@ -13,7 +13,9 @@ import capellacollab.projects.toolmodels.models import capellacollab.projects.toolmodels.modelsources.git.models import capellacollab.projects.toolmodels.modelsources.t4c.models +import capellacollab.projects.toolmodels.provisioning.models import capellacollab.projects.toolmodels.restrictions.models +import capellacollab.projects.tools.models import capellacollab.projects.users.models import capellacollab.sessions.models import capellacollab.settings.configuration.models diff --git a/backend/capellacollab/core/responses.py b/backend/capellacollab/core/responses.py index 343a8ab856..5caed9b193 100644 --- a/backend/capellacollab/core/responses.py +++ b/backend/capellacollab/core/responses.py @@ -210,3 +210,33 @@ class ZIPFileResponse(fastapi.responses.StreamingResponse): } } } + + +class MarkdownResponse(fastapi.responses.Response): + """Custom error class for Markdown responses. + + To use the class as response class, pass the following parameters + to the fastapi route definition. + + ```python + response_class=fastapi.responses.Response + responses=responses.MarkdownResponse.responses + ``` + + Don't use Markdown as response_class as this will also change the + media type for all error responses, see: + https://github.com/tiangolo/fastapi/discussions/6799 + + To return an Markdown response in the route, use: + + ```python + return responses.MarkdownResponse( + content=b"# Hello World", + ) + ``` + """ + + media_type = "text/markdown" + responses: dict[int | str, dict[str, t.Any]] | None = { + 200: {"content": {"text/markdown": {"schema": {"type": "string"}}}} + } diff --git a/backend/capellacollab/projects/models.py b/backend/capellacollab/projects/models.py index 0927ba6253..19139c31a5 100644 --- a/backend/capellacollab/projects/models.py +++ b/backend/capellacollab/projects/models.py @@ -16,6 +16,9 @@ if t.TYPE_CHECKING: from capellacollab.projects.toolmodels.models import DatabaseToolModel + from capellacollab.projects.tools.models import ( + DatabaseProjectToolAssociation, + ) from capellacollab.projects.users.models import ProjectUserAssociation @@ -133,5 +136,8 @@ class DatabaseProject(database.Base): models: orm.Mapped[list[DatabaseToolModel]] = orm.relationship( default_factory=list, back_populates="project" ) + tools: orm.Mapped[list[DatabaseProjectToolAssociation]] = orm.relationship( + default_factory=list, back_populates="project" + ) is_archived: orm.Mapped[bool] = orm.mapped_column(default=False) diff --git a/backend/capellacollab/projects/routes.py b/backend/capellacollab/projects/routes.py index a26c4f9414..91d4e81229 100644 --- a/backend/capellacollab/projects/routes.py +++ b/backend/capellacollab/projects/routes.py @@ -18,6 +18,7 @@ from capellacollab.projects.toolmodels.backups import core as backups_core from capellacollab.projects.toolmodels.backups import crud as backups_crud from capellacollab.projects.toolmodels.backups import models as backups_models +from capellacollab.projects.tools import routes as projects_tools_routes from capellacollab.projects.users import crud as projects_users_crud from capellacollab.projects.users import models as projects_users_models from capellacollab.projects.users import routes as projects_users_routes @@ -205,3 +206,8 @@ def _delete_all_pipelines_for_project( prefix="/{project_slug}/events", tags=["Projects - Events"], ) +router.include_router( + projects_tools_routes.router, + prefix="/{project_slug}/tools", + tags=["Projects - Tools"], +) diff --git a/backend/capellacollab/projects/toolmodels/backups/runs/interface.py b/backend/capellacollab/projects/toolmodels/backups/runs/interface.py index 2d8dea86a4..a433b0b77b 100644 --- a/backend/capellacollab/projects/toolmodels/backups/runs/interface.py +++ b/backend/capellacollab/projects/toolmodels/backups/runs/interface.py @@ -270,6 +270,7 @@ def _job_is_finished(status: models.PipelineRunStatus): def _refresh_and_trigger_pipeline_jobs(): + log.debug("Starting to refresh and trigger pipeline jobs...") _schedule_pending_jobs() with database.SessionLocal() as db: for run in crud.get_scheduled_or_running_pipelines(db): @@ -301,3 +302,4 @@ def _refresh_and_trigger_pipeline_jobs(): _terminate_job(run) db.commit() + log.debug("Finished refreshing and triggering of pipeline jobs.") diff --git a/backend/capellacollab/projects/toolmodels/crud.py b/backend/capellacollab/projects/toolmodels/crud.py index b26bf79b17..305db5ab4e 100644 --- a/backend/capellacollab/projects/toolmodels/crud.py +++ b/backend/capellacollab/projects/toolmodels/crud.py @@ -7,7 +7,7 @@ import sqlalchemy as sa from sqlalchemy import orm -from capellacollab.projects import models as projects_model +from capellacollab.projects import models as projects_models from capellacollab.tools import models as tools_models from . import models @@ -68,7 +68,7 @@ def get_model_by_slugs( .options(orm.joinedload(models.DatabaseToolModel.project)) .where( models.DatabaseToolModel.project.has( - projects_model.DatabaseProject.slug == project_slug + projects_models.DatabaseProject.slug == project_slug ) ) .where(models.DatabaseToolModel.slug == model_slug) @@ -77,7 +77,7 @@ def get_model_by_slugs( def create_model( db: orm.Session, - project: projects_model.DatabaseProject, + project: projects_models.DatabaseProject, post_model: models.PostToolModel, tool: tools_models.DatabaseTool, version: tools_models.DatabaseVersion | None = None, @@ -135,7 +135,7 @@ def update_model( name: str | None, version: tools_models.DatabaseVersion | None, nature: tools_models.DatabaseNature | None, - project: projects_model.DatabaseProject, + project: projects_models.DatabaseProject, display_order: int | None, ) -> models.DatabaseToolModel: model.version = version diff --git a/backend/capellacollab/projects/toolmodels/modelbadge/exceptions.py b/backend/capellacollab/projects/toolmodels/modelbadge/exceptions.py index 9b37f7528a..ea26301d97 100644 --- a/backend/capellacollab/projects/toolmodels/modelbadge/exceptions.py +++ b/backend/capellacollab/projects/toolmodels/modelbadge/exceptions.py @@ -13,7 +13,7 @@ def __init__(self): title="Model complexity badge not configured properly", reason=( "The model complexity badge is not configured properly. " - "Please contact your diagram cache administrator." + "Please contact your project admin or system administrator." ), err_code="MODEL_COMPLEXITY_BADGE_NOT_CONFIGURED_PROPERLY", ) diff --git a/backend/capellacollab/projects/toolmodels/models.py b/backend/capellacollab/projects/toolmodels/models.py index 1eb94bdc16..ac9a057a64 100644 --- a/backend/capellacollab/projects/toolmodels/models.py +++ b/backend/capellacollab/projects/toolmodels/models.py @@ -37,6 +37,7 @@ DatabaseVersion, ) + from .provisioning.models import DatabaseModelProvisioning from .restrictions.models import DatabaseToolModelRestrictions @@ -124,6 +125,14 @@ class DatabaseToolModel(database.Base): ) ) + provisioning: orm.Mapped[list[DatabaseModelProvisioning]] = ( + orm.relationship( + back_populates="tool_model", + cascade="delete", + default_factory=list, + ) + ) + class ToolModel(core_pydantic.BaseModel): id: int @@ -144,3 +153,10 @@ class SimpleToolModel(core_pydantic.BaseModel): slug: str name: str project: projects_models.SimpleProject + + +class SimpleToolModelWithoutProject(core_pydantic.BaseModel): + id: int + slug: str + name: str + git_models: list[GitModel] | None = None diff --git a/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py b/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py index 42339c2153..d5d7a0d190 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/git/routes.py @@ -14,7 +14,7 @@ from capellacollab.projects.toolmodels import models as toolmodels_models from capellacollab.projects.toolmodels.backups import crud as backups_crud from capellacollab.projects.users import models as projects_users_models -from capellacollab.settings.modelsources.git import core as git_core +from capellacollab.settings.modelsources.git import core as instances_git_core from capellacollab.settings.modelsources.git import models as git_models from capellacollab.settings.modelsources.git import util as git_util @@ -69,7 +69,7 @@ async def get_revisions_of_primary_git_model( injectables.get_existing_primary_git_model ), ) -> git_models.GetRevisionsResponseModel: - return await git_core.get_remote_refs( + return await instances_git_core.get_remote_refs( primary_git_model.path, primary_git_model.username, primary_git_model.password, @@ -94,7 +94,7 @@ async def get_revisions_with_model_credentials( injectables.get_existing_git_model ), ): - return await git_core.get_remote_refs( + return await instances_git_core.get_remote_refs( url, git_model.username, git_model.password ) diff --git a/backend/capellacollab/projects/toolmodels/provisioning/__init__.py b/backend/capellacollab/projects/toolmodels/provisioning/__init__.py new file mode 100644 index 0000000000..04412280d8 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 diff --git a/backend/capellacollab/projects/toolmodels/provisioning/crud.py b/backend/capellacollab/projects/toolmodels/provisioning/crud.py new file mode 100644 index 0000000000..d82a393850 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/crud.py @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import sqlalchemy as sa +from sqlalchemy import orm + +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.users import models as users_models + +from . import models + + +def create_model_provisioning( + db: orm.Session, model: models.DatabaseModelProvisioning +) -> models.DatabaseModelProvisioning: + db.add(model) + db.commit() + return model + + +def get_model_provisioning( + db: orm.Session, + tool_model: toolmodels_models.DatabaseToolModel, + user: users_models.DatabaseUser, +) -> models.DatabaseModelProvisioning | None: + return db.execute( + sa.select(models.DatabaseModelProvisioning) + .where(models.DatabaseModelProvisioning.tool_model == tool_model) + .where(models.DatabaseModelProvisioning.user == user) + ).scalar_one_or_none() + + +def delete_model_provisioning( + db: orm.Session, provisioning: models.DatabaseModelProvisioning +): + db.delete(provisioning) + db.commit() diff --git a/backend/capellacollab/projects/toolmodels/provisioning/exceptions.py b/backend/capellacollab/projects/toolmodels/provisioning/exceptions.py new file mode 100644 index 0000000000..2ba4c5dbff --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/exceptions.py @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from fastapi import status + +from capellacollab.core import exceptions as core_exceptions + + +class ProvisioningNotFoundError(core_exceptions.BaseError): + def __init__(self, project_slug: str, model_slug: str): + super().__init__( + status_code=status.HTTP_404_NOT_FOUND, + title="Provisioning not found", + reason=f"Couldn't find a provisioning for the model '{model_slug}' in the project '{project_slug}'.", + err_code="PROVISIONING_NOT_FOUND", + ) diff --git a/backend/capellacollab/projects/toolmodels/provisioning/injectables.py b/backend/capellacollab/projects/toolmodels/provisioning/injectables.py new file mode 100644 index 0000000000..c1802084e4 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/injectables.py @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +import fastapi +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.users import injectables as users_injectables +from capellacollab.users import models as users_models + +from .. import injectables as toolmodels_injectables +from .. import models as toolmodels_models +from . import crud, models + + +def get_model_provisioning( + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( + toolmodels_injectables.get_existing_capella_model + ), + current_user: users_models.DatabaseUser = fastapi.Depends( + users_injectables.get_own_user + ), + db: orm.Session = fastapi.Depends(database.get_db), +) -> models.DatabaseModelProvisioning | None: + return crud.get_model_provisioning(db, tool_model=model, user=current_user) diff --git a/backend/capellacollab/projects/toolmodels/provisioning/models.py b/backend/capellacollab/projects/toolmodels/provisioning/models.py new file mode 100644 index 0000000000..1bd75d5c23 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/models.py @@ -0,0 +1,66 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import datetime + +import pydantic +import sqlalchemy as sa +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.core import pydantic as core_pydantic +from capellacollab.projects.toolmodels import ( + models as projects_toolmodels_models, +) +from capellacollab.sessions import models as sessions_models +from capellacollab.users import models as users_models + + +class ModelProvisioning(core_pydantic.BaseModel): + session: sessions_models.Session | None + provisioned_at: datetime.datetime + revision: str + commit_hash: str + + _validate_trigger_time = pydantic.field_serializer("provisioned_at")( + core_pydantic.datetime_serializer + ) + + +class DatabaseModelProvisioning(database.Base): + __tablename__ = "model_provisioning" + + id: orm.Mapped[int] = orm.mapped_column( + init=False, primary_key=True, index=True + ) + + user_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("users.id"), + init=False, + ) + user: orm.Mapped[users_models.DatabaseUser] = orm.relationship( + foreign_keys=[user_id] + ) + + tool_model_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("models.id"), + init=False, + ) + tool_model: orm.Mapped[projects_toolmodels_models.DatabaseToolModel] = ( + orm.relationship( + foreign_keys=[tool_model_id], + ) + ) + + revision: orm.Mapped[str] + commit_hash: orm.Mapped[str] + + provisioned_at: orm.Mapped[datetime.datetime] = orm.mapped_column( + default=datetime.datetime.now(datetime.UTC) + ) + + session: orm.Mapped[sessions_models.DatabaseSession | None] = ( + orm.relationship( + uselist=False, back_populates="provisioning", default=None + ) + ) diff --git a/backend/capellacollab/projects/toolmodels/provisioning/routes.py b/backend/capellacollab/projects/toolmodels/provisioning/routes.py new file mode 100644 index 0000000000..44ee7152b9 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/routes.py @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import fastapi +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.core.authentication import injectables as auth_injectables +from capellacollab.projects import injectables as projects_injectables +from capellacollab.projects import models as projects_models +from capellacollab.projects.toolmodels import ( + injectables as toolmodels_injectables, +) +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.users import models as projects_users_models + +from . import crud, exceptions, injectables, models + +router = fastapi.APIRouter( + dependencies=[ + fastapi.Depends( + auth_injectables.ProjectRoleVerification( + required_role=projects_users_models.ProjectUserRole.USER + ) + ) + ], +) + + +@router.get("", response_model=models.ModelProvisioning | None) +def get_provisioning( + provisioning: models.DatabaseModelProvisioning = fastapi.Depends( + injectables.get_model_provisioning + ), +) -> models.DatabaseModelProvisioning: + return provisioning + + +@router.delete("", status_code=204) +def reset_provisioning( + provisioning: models.DatabaseModelProvisioning | None = fastapi.Depends( + injectables.get_model_provisioning + ), + model: toolmodels_models.DatabaseToolModel = fastapi.Depends( + toolmodels_injectables.get_existing_capella_model + ), + project: projects_models.DatabaseProject = fastapi.Depends( + projects_injectables.get_existing_project + ), + db: orm.Session = fastapi.Depends(database.get_db), +): + """This will delete the provisioning data from the workspace. + During the next session request, the existing provisioning will be overwritten in the workspace. + """ + if not provisioning: + raise exceptions.ProvisioningNotFoundError( + project_slug=project.slug, model_slug=model.slug + ) + + crud.delete_model_provisioning(db, provisioning) diff --git a/backend/capellacollab/projects/toolmodels/readme/routes.py b/backend/capellacollab/projects/toolmodels/readme/routes.py new file mode 100644 index 0000000000..ca0b16be63 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/readme/routes.py @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import logging + +import fastapi + +import capellacollab.projects.toolmodels.modelsources.git.injectables as git_injectables +from capellacollab.core import logging as log +from capellacollab.core import responses +from capellacollab.core.authentication import injectables as auth_injectables +from capellacollab.projects.toolmodels.modelsources.git.handler import handler +from capellacollab.projects.users import models as projects_users_models + +router = fastapi.APIRouter( + dependencies=[ + fastapi.Depends( + auth_injectables.ProjectRoleVerification( + required_role=projects_users_models.ProjectUserRole.USER + ) + ) + ], +) + + +@router.get( + "", + response_class=fastapi.responses.Response, + responses=responses.MarkdownResponse.responses, +) +async def get_readme( + git_handler: handler.GitHandler = fastapi.Depends( + git_injectables.get_git_handler + ), + logger: logging.LoggerAdapter = fastapi.Depends(log.get_request_logger), +): + _, file = await git_handler.get_file("README.md", logger, None) + return responses.MarkdownResponse(content=file) diff --git a/backend/capellacollab/projects/toolmodels/routes.py b/backend/capellacollab/projects/toolmodels/routes.py index 714d980d14..2c05586b4b 100644 --- a/backend/capellacollab/projects/toolmodels/routes.py +++ b/backend/capellacollab/projects/toolmodels/routes.py @@ -23,6 +23,8 @@ from .diagrams import routes as diagrams_routes from .modelbadge import routes as complexity_badge_routes from .modelsources import routes as modelsources_routes +from .provisioning import routes as provisioning_routes +from .readme import routes as readme_routes from .restrictions import routes as restrictions_routes router = fastapi.APIRouter( @@ -269,3 +271,13 @@ def raise_if_model_exists_in_project( prefix="/{model_slug}/badges/complexity", tags=["Projects - Models - Model complexity badge"], ) +router.include_router( + provisioning_routes.router, + prefix="/{model_slug}/provisioning", + tags=["Projects - Models - Provisioning"], +) +router.include_router( + readme_routes.router, + prefix="/{model_slug}/readme", + tags=["Projects - Models - README"], +) diff --git a/backend/capellacollab/projects/tools/__init__.py b/backend/capellacollab/projects/tools/__init__.py new file mode 100644 index 0000000000..04412280d8 --- /dev/null +++ b/backend/capellacollab/projects/tools/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 diff --git a/backend/capellacollab/projects/tools/crud.py b/backend/capellacollab/projects/tools/crud.py new file mode 100644 index 0000000000..287f453927 --- /dev/null +++ b/backend/capellacollab/projects/tools/crud.py @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import sqlalchemy as sa +from sqlalchemy import orm + +from capellacollab.projects import models as projects_models +from capellacollab.tools import models as tools_models + +from . import models + + +def create_project_tool( + db: orm.Session, + project: projects_models.DatabaseProject, + tool_version: tools_models.DatabaseVersion, +) -> models.DatabaseProjectToolAssociation: + project_tool = models.DatabaseProjectToolAssociation( + project=project, tool_version=tool_version + ) + db.add(project_tool) + db.commit() + db.refresh(project_tool) + return project_tool + + +def get_project_tool_by_id( + db: orm.Session, + project_tool_id: int, +) -> models.DatabaseProjectToolAssociation | None: + return db.execute( + sa.select(models.DatabaseProjectToolAssociation).where( + models.DatabaseProjectToolAssociation.id == project_tool_id + ) + ).scalar_one_or_none() + + +def get_project_tool_by_project_and_tool_version( + db: orm.Session, + project: projects_models.DatabaseProject, + tool_version: tools_models.DatabaseVersion, +) -> models.DatabaseProjectToolAssociation | None: + return db.execute( + sa.select(models.DatabaseProjectToolAssociation) + .where( + models.DatabaseProjectToolAssociation.tool_version == tool_version + ) + .where(models.DatabaseProjectToolAssociation.project == project) + ).scalar_one_or_none() + + +def delete_project_tool( + db: orm.Session, project_tool: models.DatabaseProjectToolAssociation +) -> None: + db.delete(project_tool) + db.commit() diff --git a/backend/capellacollab/projects/tools/exceptions.py b/backend/capellacollab/projects/tools/exceptions.py new file mode 100644 index 0000000000..0fb894ccf6 --- /dev/null +++ b/backend/capellacollab/projects/tools/exceptions.py @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from fastapi import status + +from capellacollab.core import exceptions as core_exceptions + + +class ProjectToolBelongsToOtherProject(core_exceptions.BaseError): + def __init__(self, project_tool_id: int, project_slug: str): + super().__init__( + status_code=status.HTTP_403_FORBIDDEN, + title="The project tool belongs to another project", + reason=f"The project tool with ID {project_tool_id} doesn't belong to the project '{project_slug}'.", + err_code="PROJECT_TOOL_DOES_NOT_BELONG_TO_PROJECT", + ) + + +class ProjectToolNotFound(core_exceptions.BaseError): + def __init__(self, project_tool_id: int): + super().__init__( + status_code=status.HTTP_404_NOT_FOUND, + title="Tool not found in project", + reason=f"The project tool with ID {project_tool_id} was not found.", + err_code="PROJECT_TOOL_NOT_FOUND", + ) + + +class ToolAlreadyLinkedToProjectError(core_exceptions.BaseError): + def __init__(self): + super().__init__( + status_code=status.HTTP_409_CONFLICT, + title="Tool already linked to project", + reason="The specific version of the tool is already linked to the project.", + err_code="TOOL_ALREADY_EXISTS_IN_PROJECT", + ) diff --git a/backend/capellacollab/projects/tools/injectables.py b/backend/capellacollab/projects/tools/injectables.py new file mode 100644 index 0000000000..139d0c3c19 --- /dev/null +++ b/backend/capellacollab/projects/tools/injectables.py @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import fastapi +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.projects import injectables as projects_injectables +from capellacollab.projects import models as projects_models + +from . import crud, exceptions, models + + +def get_existing_project_tool( + project_tool_id: int, + db: orm.Session = fastapi.Depends(database.get_db), + project: projects_models.DatabaseProject = fastapi.Depends( + projects_injectables.get_existing_project + ), +) -> models.DatabaseProjectToolAssociation: + project_tool = crud.get_project_tool_by_id(db, project_tool_id) + if not project_tool: + raise exceptions.ProjectToolNotFound(project_tool_id) + if project_tool.project != project: + raise exceptions.ProjectToolBelongsToOtherProject( + project_tool_id, project.slug + ) + return project_tool diff --git a/backend/capellacollab/projects/tools/models.py b/backend/capellacollab/projects/tools/models.py new file mode 100644 index 0000000000..87a9b7a828 --- /dev/null +++ b/backend/capellacollab/projects/tools/models.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +import typing as t + +import pydantic +import sqlalchemy as sa +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.core import pydantic as core_pydantic +from capellacollab.projects.toolmodels import models as toolsmodels_models +from capellacollab.tools import models as tools_models + +if t.TYPE_CHECKING: + from capellacollab.projects.models import DatabaseProject + from capellacollab.tools.models import DatabaseVersion + + +class ProjectTool(core_pydantic.BaseModel): + id: int | None + + tool_version: tools_models.SimpleToolVersion + tool: tools_models.Tool + used_by: list[toolsmodels_models.SimpleToolModelWithoutProject] = [] + + @pydantic.model_validator(mode="before") + @classmethod + def derive_tool_from_version(cls, data: t.Any) -> t.Any: + if not isinstance(data, DatabaseProjectToolAssociation): + return data + + data_dict = data.__dict__ + data_dict["tool"] = data.tool_version.tool + return data_dict + + +class PostProjectToolRequest(core_pydantic.BaseModel): + tool_id: int + tool_version_id: int + + +class DatabaseProjectToolAssociation(database.Base): + __tablename__ = "project_tool_association" + + id: orm.Mapped[int] = orm.mapped_column( + sa.Integer, + init=False, + primary_key=True, + autoincrement=True, + ) + + project_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("projects.id"), primary_key=True, init=False + ) + project: orm.Mapped["DatabaseProject"] = orm.relationship( + back_populates="tools" + ) + + tool_version_id: orm.Mapped[int] = orm.mapped_column( + sa.ForeignKey("versions.id"), primary_key=True, init=False + ) + tool_version: orm.Mapped["DatabaseVersion"] = orm.relationship() diff --git a/backend/capellacollab/projects/tools/routes.py b/backend/capellacollab/projects/tools/routes.py new file mode 100644 index 0000000000..8bb3570df3 --- /dev/null +++ b/backend/capellacollab/projects/tools/routes.py @@ -0,0 +1,117 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import fastapi +from sqlalchemy import orm + +from capellacollab.core import database +from capellacollab.core.authentication import injectables as auth_injectables +from capellacollab.projects import injectables as projects_injectables +from capellacollab.projects import models as projects_models +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.users import models as projects_users_models +from capellacollab.tools import injectables as tools_injectables +from capellacollab.tools import models as tools_models + +from . import crud, exceptions, injectables, models + +router = fastapi.APIRouter( + dependencies=[ + fastapi.Depends( + auth_injectables.ProjectRoleVerification( + required_role=projects_users_models.ProjectUserRole.USER + ) + ) + ] +) + + +@router.get( + "", +) +def get_project_tools( + project: projects_models.DatabaseProject = fastapi.Depends( + projects_injectables.get_existing_project + ), +) -> list[models.ProjectTool]: + tools = [models.ProjectTool.model_validate(tool) for tool in project.tools] + + for model in project.models: + if not model.version: + continue + + tool = next( + ( + tool + for tool in tools + if model.version.id == tool.tool_version.id + ), + None, + ) + + if not tool: + tool = models.ProjectTool( + id=None, + tool_version=tools_models.SimpleToolVersion.model_validate( + model.version + ), + tool=tools_models.Tool.model_validate(model.version.tool), + used_by=[], + ) + tools.append(tool) + + tool.used_by.append( + toolmodels_models.SimpleToolModelWithoutProject.model_validate( + model + ) + ) + + return tools + + +@router.post( + "", + response_model=models.ProjectTool, + dependencies=[ + fastapi.Depends( + auth_injectables.ProjectRoleVerification( + required_role=projects_users_models.ProjectUserRole.MANAGER + ) + ) + ], +) +def link_tool_to_project( + body: models.PostProjectToolRequest, + db: orm.Session = fastapi.Depends(database.get_db), + project: projects_models.DatabaseProject = fastapi.Depends( + projects_injectables.get_existing_project + ), +) -> models.DatabaseProjectToolAssociation: + tool_version = tools_injectables.get_existing_tool_version( + body.tool_id, body.tool_version_id, db + ) + if crud.get_project_tool_by_project_and_tool_version( + db, project, tool_version + ): + raise exceptions.ToolAlreadyLinkedToProjectError() + return crud.create_project_tool(db, project, tool_version) + + +@router.delete( + "/{project_tool_id}", + status_code=204, + dependencies=[ + fastapi.Depends( + auth_injectables.ProjectRoleVerification( + required_role=projects_users_models.ProjectUserRole.MANAGER + ) + ) + ], +) +def delete_tool_from_project( + db: orm.Session = fastapi.Depends(database.get_db), + project_tool: models.DatabaseProjectToolAssociation = fastapi.Depends( + injectables.get_existing_project_tool + ), +) -> None: + crud.delete_project_tool(db, project_tool) diff --git a/backend/capellacollab/sessions/exceptions.py b/backend/capellacollab/sessions/exceptions.py index 938f8b61ad..3291ca4801 100644 --- a/backend/capellacollab/sessions/exceptions.py +++ b/backend/capellacollab/sessions/exceptions.py @@ -115,6 +115,22 @@ def __init__( ) +class ProjectAndModelMismatchError(core_exceptions.BaseError): + def __init__( + self, + project_slug: str, + model_name: str, + ): + super().__init__( + status_code=status.HTTP_409_CONFLICT, + title="Mismatch between project scope and provisioning", + reason=( + f"The model '{model_name}' doesn't belong to the project '{project_slug}'." + ), + err_code="MODEL_PROJECT_MISMATCH", + ) + + class InvalidConnectionMethodIdentifierError(core_exceptions.BaseError): def __init__( self, @@ -166,3 +182,23 @@ def __init__(self): reason="Provisioning is not supported for persistent sessions.", err_code="PROVISIONING_UNSUPPORTED", ) + + +class ProvisioningRequiredError(core_exceptions.BaseError): + def __init__(self): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + title="Provisioning is required for this tool", + reason="Provisioning is required for persistent sessions of the selected tool.", + err_code="PROVISIONING_REQUIRED", + ) + + +class ProjectScopeRequiredError(core_exceptions.BaseError): + def __init__(self): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + title="A project scope is required.", + reason="Persistent provisioning requires a project scope.", + err_code="PROJECT_SCOPE_REQUIRED", + ) diff --git a/backend/capellacollab/sessions/hooks/__init__.py b/backend/capellacollab/sessions/hooks/__init__.py index 09d7de7904..bfb3bfbbf7 100644 --- a/backend/capellacollab/sessions/hooks/__init__.py +++ b/backend/capellacollab/sessions/hooks/__init__.py @@ -11,6 +11,7 @@ jupyter, networking, persistent_workspace, + project_scope, provisioning, pure_variants, read_only_workspace, @@ -29,6 +30,7 @@ "guacamole": guacamole.GuacamoleIntegration(), "http": http.HTTPIntegration(), "read_only_hook": read_only_workspace.ReadOnlyWorkspaceHook(), + "project_scope": project_scope.ProjectScopeHook(), "provisioning": provisioning.ProvisionWorkspaceHook(), "session_preparation": session_preparation.GitRepositoryCloningHook(), "networking": networking.NetworkingIntegration(), diff --git a/backend/capellacollab/sessions/hooks/authentication.py b/backend/capellacollab/sessions/hooks/authentication.py index 16b0aaf2f3..5a8d4a564c 100644 --- a/backend/capellacollab/sessions/hooks/authentication.py +++ b/backend/capellacollab/sessions/hooks/authentication.py @@ -13,18 +13,16 @@ class PreAuthenticationHook(interface.HookRegistration): - def session_connection_hook( # type: ignore[override] + def session_connection_hook( self, - db_session: sessions_models.DatabaseSession, - user: users_models.DatabaseUser, - **kwargs, + request: interface.SessionConnectionHookRequest, ) -> interface.SessionConnectionHookResult: """Issue pre-authentication tokens for sessions""" return interface.SessionConnectionHookResult( cookies={ "ccm_session_token": self._issue_session_token( - user, db_session + request.user, request.db_session ) } ) diff --git a/backend/capellacollab/sessions/hooks/guacamole.py b/backend/capellacollab/sessions/hooks/guacamole.py index dce8f001c6..4ea12c157c 100644 --- a/backend/capellacollab/sessions/hooks/guacamole.py +++ b/backend/capellacollab/sessions/hooks/guacamole.py @@ -12,9 +12,6 @@ from capellacollab.config import config from capellacollab.core import credentials -from capellacollab.sessions import models as sessions_models -from capellacollab.sessions.operators import k8s -from capellacollab.tools import models as tools_models from . import interface @@ -40,14 +37,11 @@ class GuacamoleIntegration(interface.HookRegistration): "https": None, } - def post_session_creation_hook( # type: ignore[override] + def post_session_creation_hook( self, - session: k8s.Session, - db_session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + request: interface.PostSessionCreationHookRequest, ) -> interface.PostSessionCreationHookResult: - if connection_method.type != "guacamole": + if request.connection_method.type != "guacamole": return interface.PostSessionCreationHookResult() guacamole_username = credentials.generate_password() @@ -60,9 +54,9 @@ def post_session_creation_hook( # type: ignore[override] guacamole_identifier = self._create_connection( guacamole_token, - db_session.environment["CAPELLACOLLAB_SESSION_TOKEN"], - session["host"], - session["port"], + request.db_session.environment["CAPELLACOLLAB_SESSION_TOKEN"], + request.session["host"], + request.session["port"], )["identifier"] self._assign_user_to_connection( @@ -79,16 +73,14 @@ def post_session_creation_hook( # type: ignore[override] config=guacamole_config, ) - def session_connection_hook( # type: ignore[override] + def session_connection_hook( self, - db_session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + request: interface.SessionConnectionHookRequest, ) -> interface.SessionConnectionHookResult: - if connection_method.type != "guacamole": + if request.connection_method.type != "guacamole": return interface.SessionConnectionHookResult() - session_config = db_session.config + session_config = request.db_session.config if not session_config or not session_config.get("guacamole_username"): return interface.SessionConnectionHookResult() @@ -102,16 +94,13 @@ def session_connection_hook( # type: ignore[override] redirect_url=config.extensions.guacamole.public_uri + "/#/", ) - def pre_session_termination_hook( # type: ignore[override] - self, - session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + def pre_session_termination_hook( + self, request: interface.PreSessionTerminationHookRequest ) -> interface.PreSessionTerminationHookResult: - if connection_method.type != "guacamole": + if request.connection_method.type != "guacamole": return interface.SessionConnectionHookResult() - session_config = session.config + session_config = request.session.config if session_config and session_config.get("guacamole_username"): guacamole_token = self._get_admin_token() diff --git a/backend/capellacollab/sessions/hooks/http.py b/backend/capellacollab/sessions/hooks/http.py index aae9953021..e97b36b15b 100644 --- a/backend/capellacollab/sessions/hooks/http.py +++ b/backend/capellacollab/sessions/hooks/http.py @@ -1,35 +1,28 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import logging - from capellacollab.core import models as core_models from capellacollab.tools import models as tools_models -from .. import models as sessions_models from .. import util as sessions_util from . import interface class HTTPIntegration(interface.HookRegistration): - def session_connection_hook( # type: ignore[override] - self, - db_session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - logger: logging.LoggerAdapter, - **kwargs, + def session_connection_hook( + self, request: interface.SessionConnectionHookRequest ) -> interface.SessionConnectionHookResult: if not isinstance( - connection_method, tools_models.HTTPConnectionMethod + request.connection_method, tools_models.HTTPConnectionMethod ): return interface.SessionConnectionHookResult() try: - redirect_url = connection_method.redirect_url.format( - **db_session.environment + redirect_url = request.connection_method.redirect_url.format( + **request.db_session.environment ) except Exception: - logger.error( + request.logger.error( "Error while formatting the redirect URL", exc_info=True ) return interface.SessionConnectionHookResult( @@ -43,7 +36,9 @@ def session_connection_hook( # type: ignore[override] ) cookies, warnings = sessions_util.resolve_environment_variables( - logger, db_session.environment, connection_method.cookies + request.logger, + request.db_session.environment, + request.connection_method.cookies, ) return interface.SessionConnectionHookResult( diff --git a/backend/capellacollab/sessions/hooks/interface.py b/backend/capellacollab/sessions/hooks/interface.py index fefbd60f4a..dced782700 100644 --- a/backend/capellacollab/sessions/hooks/interface.py +++ b/backend/capellacollab/sessions/hooks/interface.py @@ -2,12 +2,14 @@ # SPDX-License-Identifier: Apache-2.0 import abc +import dataclasses import logging import typing as t from sqlalchemy import orm from capellacollab.core import models as core_models +from capellacollab.projects import models as projects_models from capellacollab.sessions import operators from capellacollab.sessions.operators import k8s from capellacollab.sessions.operators import models as operators_models @@ -17,6 +19,44 @@ from .. import models as sessions_models +@dataclasses.dataclass() +class ConfigurationHookRequest: + """Request type of the configuration hook + + Attributes + ---------- + db : sqlalchemy.orm.Session + Database session. Can be used to access the database + operator : operators.KubernetesOperator + Operator, which is used to spawn the session + user : users_models.DatabaseUser + User who has requested the session + tool : tools_models.DatabaseTool + Tool of the requested session + tool_version : tools_models.DatabaseVersion + Tool version of the requested session + session_type : sessions_models.SessionType + Type of the session (persistent, read-only, etc.) + connection_method : tools_models.ToolSessionConnectionMethod + Requested connection method for the session + provisioning : list[sessions_models.SessionProvisioningRequest] + List of workspace provisioning requests + session_id: str + ID of the session to be created + """ + + db: orm.Session + operator: operators.KubernetesOperator + user: users_models.DatabaseUser + tool: tools_models.DatabaseTool + tool_version: tools_models.DatabaseVersion + session_type: sessions_models.SessionType + connection_method: tools_models.ToolSessionConnectionMethod + provisioning: list[sessions_models.SessionProvisioningRequest] + project_scope: projects_models.DatabaseProject | None + session_id: str + + class ConfigurationHookResult(t.TypedDict): """Return type of the configuration hook @@ -42,6 +82,34 @@ class ConfigurationHookResult(t.TypedDict): init_environment: t.NotRequired[t.Mapping] +@dataclasses.dataclass() +class PostSessionCreationHookRequest: + """Request type of the post session creation hook + + Attributes + ---------- + session_id : str + ID of the session + session : k8s.Session + Session object (contains connection information) + db_session : sessions_models.DatabaseSession + Collaboration Manager session in the database + operator : operators.KubernetesOperator + Operator, which is used to spawn the session + user : users_models.DatabaseUser + User who has requested the session + connection_method : tools_models.ToolSessionConnectionMethod + Requested connection method for the session + """ + + session_id: str + session: k8s.Session + db_session: sessions_models.DatabaseSession + operator: operators.KubernetesOperator + user: users_models.DatabaseUser + connection_method: tools_models.ToolSessionConnectionMethod + + class PostSessionCreationHookResult(t.TypedDict): """Return type of the post session creation hook @@ -55,6 +123,31 @@ class PostSessionCreationHookResult(t.TypedDict): config: t.NotRequired[t.Mapping] +@dataclasses.dataclass() +class SessionConnectionHookRequest: + """Request type of the session connection hook + + Attributes + ---------- + db : sqlalchemy.orm.Session + Database session. Can be used to access the database + db_session : sessions_models.DatabaseSession + Collaboration Manager session in the database + connection_method : tools_models.ToolSessionConnectionMethod + Connection method of the session + logger : logging.LoggerAdapter + Logger for the specific request + user : users_models.DatabaseUser + User who is connecting to the session + """ + + db: orm.Session + db_session: sessions_models.DatabaseSession + connection_method: tools_models.ToolSessionConnectionMethod + logger: logging.LoggerAdapter + user: users_models.DatabaseUser + + class SessionConnectionHookResult(t.TypedDict): """Return type of the session connection hook @@ -80,6 +173,28 @@ class SessionConnectionHookResult(t.TypedDict): warnings: t.NotRequired[list[core_models.Message]] +@dataclasses.dataclass() +class PreSessionTerminationHookRequest: + """Request type of the pre session termination hook + + Attributes + ---------- + db : sqlalchemy.orm.Session + Database session. Can be used to access the database + operator : operators.KubernetesOperator + Operator, which is used to spawn the session + session : sessions_models.DatabaseSession + Session which is to be terminated + connection_method : tools_models.ToolSessionConnectionMethod + Connection method of the session + """ + + db: orm.Session + operator: operators.KubernetesOperator + session: sessions_models.DatabaseSession + connection_method: tools_models.ToolSessionConnectionMethod + + class PreSessionTerminationHookResult(t.TypedDict): """Return type of the pre session termination hook""" @@ -101,143 +216,58 @@ class HookRegistration(metaclass=abc.ABCMeta): # pylint: disable=unused-argument def configuration_hook( - self, - db: orm.Session, - operator: operators.KubernetesOperator, - user: users_models.DatabaseUser, - tool: tools_models.DatabaseTool, - tool_version: tools_models.DatabaseVersion, - session_type: sessions_models.SessionType, - connection_method: tools_models.ToolSessionConnectionMethod, - provisioning: list[sessions_models.SessionProvisioningRequest], - session_id: str, - **kwargs, + self, request: ConfigurationHookRequest ) -> ConfigurationHookResult: """Hook to determine session configuration This hook is executed before the creation of persistent sessions. + """ - Parameters - ---------- - db : sqlalchemy.orm.Session - Database session. Can be used to access the database - operator : operators.KubernetesOperator - Operator, which is used to spawn the session - user : users_models.DatabaseUser - User who has requested the session - tool : tools_models.DatabaseTool - Tool of the requested session - tool_version : tools_models.DatabaseVersion - Tool version of the requested session - session_type : sessions_models.SessionType - Type of the session (persistent, read-only, etc.) - connection_method : tools_models.ToolSessionConnectionMethod - Requested connection method for the session - provisioning : list[sessions_models.SessionProvisioningRequest] - List of workspace provisioning requests - session_id: str - ID of the session to be created - Returns - ------- - result : ConfigurationHookResult + return ConfigurationHookResult() + + # pylint: disable=unused-argument + async def async_configuration_hook( + self, request: ConfigurationHookRequest + ) -> ConfigurationHookResult: + """Hook to determine session configuration + + Same as configuration_hook, but async. """ return ConfigurationHookResult() + # pylint: disable=unused-argument def post_session_creation_hook( self, - session_id: str, - session: k8s.Session, - db_session: sessions_models.DatabaseSession, - operator: operators.KubernetesOperator, - user: users_models.DatabaseUser, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + request: PostSessionCreationHookRequest, ) -> PostSessionCreationHookResult: """Hook executed after session creation This hook is executed after a persistent session was created by the operator. - - Parameters - ---------- - session_id : str - ID of the session - session : k8s.Session - Session object (contains connection information) - db_session : sessions_models.DatabaseSession - Collaboration Manager session in the database - operator : operators.KubernetesOperator - Operator, which is used to spawn the session - user : users_models.DatabaseUser - User who has requested the session - connection_method : tools_models.ToolSessionConnectionMethod - Requested connection method for the session - - Returns - ------- - result : PostSessionCreationHookResult """ return PostSessionCreationHookResult() # pylint: disable=unused-argument def session_connection_hook( - self, - db: orm.Session, - db_session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - logger: logging.LoggerAdapter, - **kwargs, + self, request: SessionConnectionHookRequest ) -> SessionConnectionHookResult: """Hook executed while connecting to a session The hook is executed each time the GET `/sessions/{session_id}/connection` endpoint is called. - - Parameters - ---------- - db : sqlalchemy.orm.Session - Database session. Can be used to access the database - db_session : sessions_models.DatabaseSession - Collaboration Manager session in the database - connection_method : tools_models.ToolSessionConnectionMethod - Connection method of the session - logger : logging.LoggerAdapter - Logger for the specific request - Returns - ------- - result : SessionConnectionHookResult """ return SessionConnectionHookResult() + # pylint: disable=unused-argument def pre_session_termination_hook( - self, - db: orm.Session, - operator: operators.KubernetesOperator, - session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + self, request: PreSessionTerminationHookRequest ) -> PreSessionTerminationHookResult: """Hook executed directly before session termination This hook is executed before a read-only or persistent session is terminated by the operator. - - Parameters - ---------- - db : sqlalchemy.orm.Session - Database session. Can be used to access the database - operator : operators.KubernetesOperator - Operator, which is used to spawn the session - session : sessions_models.DatabaseSession - Session which is to be terminated - connection_method : tools_models.ToolSessionConnectionMethod - Connection method of the session - - Returns - ------- - result : PreSessionTerminationHookResult """ return PreSessionTerminationHookResult() diff --git a/backend/capellacollab/sessions/hooks/jupyter.py b/backend/capellacollab/sessions/hooks/jupyter.py index 4e0fa7050a..5e208291da 100644 --- a/backend/capellacollab/sessions/hooks/jupyter.py +++ b/backend/capellacollab/sessions/hooks/jupyter.py @@ -14,7 +14,6 @@ from capellacollab.sessions import operators from capellacollab.sessions.operators import models as operators_models from capellacollab.tools import models as tools_models -from capellacollab.users import models as users_models from . import interface @@ -22,16 +21,12 @@ class JupyterIntegration(interface.HookRegistration): - def configuration_hook( # type: ignore[override] + def configuration_hook( self, - db: orm.Session, - user: users_models.DatabaseUser, - tool: tools_models.DatabaseTool, - operator: operators.KubernetesOperator, - **kwargs, + request: interface.ConfigurationHookRequest, ) -> interface.ConfigurationHookResult: volumes, warnings = self._get_project_share_volume_mounts( - db, user.name, tool, operator + request.db, request.user.name, request.tool, request.operator ) return interface.ConfigurationHookResult( volumes=volumes, warnings=warnings diff --git a/backend/capellacollab/sessions/hooks/networking.py b/backend/capellacollab/sessions/hooks/networking.py index fb0a36ca6e..2561d68564 100644 --- a/backend/capellacollab/sessions/hooks/networking.py +++ b/backend/capellacollab/sessions/hooks/networking.py @@ -2,37 +2,26 @@ # SPDX-License-Identifier: Apache-2.0 -from capellacollab.sessions import operators -from capellacollab.users import models as users_models - -from .. import models as sessions_models from . import interface class NetworkingIntegration(interface.HookRegistration): """Allow sessions of the same user to talk to each other.""" - def post_session_creation_hook( # type: ignore - self, - session_id: str, - operator: operators.KubernetesOperator, - user: users_models.DatabaseUser, - **kwargs, + def post_session_creation_hook( + self, request: interface.PostSessionCreationHookRequest ) -> interface.PostSessionCreationHookResult: """Allow sessions of the user to talk to each other.""" - operator.create_network_policy_from_pod_to_label( - session_id, - {"capellacollab/session-id": session_id}, - {"capellacollab/owner-id": str(user.id)}, + request.operator.create_network_policy_from_pod_to_label( + request.session_id, + {"capellacollab/session-id": request.session_id}, + {"capellacollab/owner-id": str(request.user.id)}, ) return interface.PostSessionCreationHookResult() - def pre_session_termination_hook( # type: ignore - self, - operator: operators.KubernetesOperator, - session: sessions_models.DatabaseSession, - **kwargs, + def pre_session_termination_hook( + self, request: interface.PreSessionTerminationHookRequest ): - operator.delete_network_policy(session.id) + request.operator.delete_network_policy(request.session.id) diff --git a/backend/capellacollab/sessions/hooks/persistent_workspace.py b/backend/capellacollab/sessions/hooks/persistent_workspace.py index 5086f57a79..98e7ab37ed 100644 --- a/backend/capellacollab/sessions/hooks/persistent_workspace.py +++ b/backend/capellacollab/sessions/hooks/persistent_workspace.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: Apache-2.0 import pathlib -import typing as t import uuid from sqlalchemy import orm @@ -19,32 +18,25 @@ from . import interface -class PersistentWorkspacEnvironment(t.TypedDict): - pass - - class PersistentWorkspaceHook(interface.HookRegistration): """Takes care of the persistent workspace of a user. Is responsible for mounting the persistent workspace into persistent sessions. """ - def configuration_hook( # type: ignore + def configuration_hook( self, - db: orm.Session, - operator: operators.KubernetesOperator, - user: users_models.DatabaseUser, - session_type: sessions_models.SessionType, - tool: tools_models.DatabaseTool, - **kwargs, + request: interface.ConfigurationHookRequest, ) -> interface.ConfigurationHookResult: - if session_type == sessions_models.SessionType.READONLY: + if request.session_type == sessions_models.SessionType.READONLY: # Skip read-only sessions, no persistent workspace needed. return interface.ConfigurationHookResult() - self._check_that_persistent_workspace_is_allowed(tool) + self._check_that_persistent_workspace_is_allowed(request.tool) - volume_name = self._create_persistent_workspace(db, operator, user) + volume_name = self._create_persistent_workspace( + request.db, request.operator, request.user + ) volume = operators_models.PersistentVolume( name="workspace", read_only=False, @@ -53,7 +45,7 @@ def configuration_hook( # type: ignore ) return interface.ConfigurationHookResult( - volumes=[volume], + volumes=[volume], init_volumes=[volume] ) def _check_that_persistent_workspace_is_allowed( diff --git a/backend/capellacollab/sessions/hooks/project_scope.py b/backend/capellacollab/sessions/hooks/project_scope.py new file mode 100644 index 0000000000..e2cc8ba0b9 --- /dev/null +++ b/backend/capellacollab/sessions/hooks/project_scope.py @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pathlib +import typing as t + +from capellacollab.projects import models as projects_models +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.toolmodels.modelsources.git import ( + models as git_models, +) +from capellacollab.sessions import models as sessions_models + +from . import interface + + +class ResolvedSessionProvisioning(t.TypedDict): + entry: sessions_models.SessionProvisioningRequest + model: toolmodels_models.DatabaseToolModel + project: projects_models.DatabaseProject + git_model: git_models.DatabaseGitModel + + +class ProjectScopeHook(interface.HookRegistration): + """Makes sure to start the session with the correct workspace.""" + + @classmethod + def configuration_hook( + cls, + request: interface.ConfigurationHookRequest, + ) -> interface.ConfigurationHookResult: + environment = {} + + if ( + request.session_type == sessions_models.SessionType.PERSISTENT + and request.project_scope + ): + environment["WORKSPACE_DIR"] = str( + pathlib.PurePosixPath("/workspace") + / request.project_scope.slug + / ("tool-" + str(request.tool_version.tool_id)) + ) + + return interface.ConfigurationHookResult(environment=environment) diff --git a/backend/capellacollab/sessions/hooks/provisioning.py b/backend/capellacollab/sessions/hooks/provisioning.py index 4d1b92c902..52c604acad 100644 --- a/backend/capellacollab/sessions/hooks/provisioning.py +++ b/backend/capellacollab/sessions/hooks/provisioning.py @@ -6,6 +6,7 @@ from sqlalchemy import orm +from capellacollab.core import models as core_models from capellacollab.core.authentication import injectables as auth_injectables from capellacollab.projects import injectables as projects_injectables from capellacollab.projects import models as projects_models @@ -19,9 +20,19 @@ from capellacollab.projects.toolmodels.modelsources.git import ( models as git_models, ) +from capellacollab.projects.toolmodels.provisioning import ( + crud as provisioning_crud, +) +from capellacollab.projects.toolmodels.provisioning import ( + models as provisioning_models, +) from capellacollab.projects.users import models as projects_users_models from capellacollab.sessions import exceptions as sessions_exceptions from capellacollab.sessions import models as sessions_models +from capellacollab.settings.modelsources.git import core as instances_git_core +from capellacollab.settings.modelsources.git import ( + exceptions as instances_git_exceptions, +) from capellacollab.tools import crud as tools_crud from capellacollab.tools import models as tools_models from capellacollab.users import models as users_models @@ -40,41 +51,155 @@ class ProvisionWorkspaceHook(interface.HookRegistration): """Takes care of the provisioning of user workspaces.""" @classmethod - def configuration_hook( # type: ignore + async def async_configuration_hook( cls, - db: orm.Session, - tool: tools_models.DatabaseTool, - tool_version: tools_models.DatabaseVersion, - user: users_models.DatabaseUser, - provisioning: list[sessions_models.SessionProvisioningRequest], - **kwargs, + request: interface.ConfigurationHookRequest, ) -> interface.ConfigurationHookResult: - max_number_of_models = tool.config.provisioning.max_number_of_models - if max_number_of_models and len(provisioning) > max_number_of_models: - raise sessions_exceptions.TooManyModelsRequestedToProvisionError( - max_number_of_models + if len(request.provisioning) == 0: + if request.tool.config.provisioning.required: + raise sessions_exceptions.ProvisioningRequiredError() + return interface.ConfigurationHookResult( + environment={"CAPELLACOLLAB_SESSION_PROVISIONING": []} ) - resolved_entries = cls._resolve_provisioning_request(db, provisioning) + cls._verify_max_number_of_models(request) + + resolved_entries = cls._resolve_provisioning_request( + request.db, request.provisioning + ) cls._verify_matching_tool_version_and_model( - db, tool_version, resolved_entries + request.db, request.tool_version, resolved_entries + ) + cls._verify_model_permissions( + request.db, request.user, resolved_entries ) - cls._verify_model_permissions(db, user, resolved_entries) - init_environment = { - "CAPELLACOLLAB_PROVISIONING": cls._get_git_repos_json( - resolved_entries, include_credentials=True + init_environment: dict[str, str] = {} + environment: dict[str, str] = {} + warnings: list[core_models.Message] = [] + if request.session_type == sessions_models.SessionType.PERSISTENT: + await cls._persistent_provisioning( + request, + resolved_entries, + init_environment, + environment, + warnings, ) - } - - environment = { - "CAPELLACOLLAB_SESSION_PROVISIONING": cls._get_git_repos_json( - resolved_entries, include_credentials=False + else: + cls._read_only_provisioning( + request, resolved_entries, init_environment, environment ) - } return interface.ConfigurationHookResult( - init_environment=init_environment, environment=environment + init_environment=init_environment, + environment=environment, + warnings=warnings, + ) + + @classmethod + async def _persistent_provisioning( + cls, + request: interface.ConfigurationHookRequest, + resolved_entries: list[ResolvedSessionProvisioning], + init_environment: dict[str, t.Any], + environment: dict[str, t.Any], + warnings: list[core_models.Message], + ): + """Provisioning for persistent sessions""" + + if not request.project_scope: + raise sessions_exceptions.ProjectScopeRequiredError() + + cls._verify_matching_project_and_model( + request.project_scope, resolved_entries + ) + + init_provisioning: list[dict[str, str | int]] = [] + session_provisioning: list[dict[str, str | int]] = [] + + for resolved_entry in resolved_entries: + existing_provisioning = provisioning_crud.get_model_provisioning( + request.db, resolved_entry["model"], request.user + ) + + entry = resolved_entry["entry"] + git_model = resolved_entry["git_model"] + + if existing_provisioning: + entry.revision = existing_provisioning.commit_hash + else: + provisioning = await cls._create_provisioning_record( + request.db, + resolved_entry, + request.user, + ) + + # Set revision to the actual commit hash + entry.revision = provisioning.commit_hash + + if not entry.deep_clone: + warnings.append( + core_models.Message( + err_code="DEEP_CLONE_REQUIRED", + title="Deep clone required.", + reason=( + "Deep clone is required for persistent provisioning." + " The provisioning will continue with deep clone." + ), + ) + ) + entry.deep_clone = True + + if not existing_provisioning: + init_provisioning.append( + cls._git_model_as_json( + git_model, + entry.revision or git_model.revision, + entry.deep_clone, + request.session_type, + include_credentials=True, + ) + ) + + session_provisioning.append( + cls._git_model_as_json( + git_model, + entry.revision or git_model.revision, + entry.deep_clone, + request.session_type, + include_credentials=False, + ) + ) + + init_environment["CAPELLACOLLAB_PROVISIONING"] = init_provisioning + environment["CAPELLACOLLAB_SESSION_PROVISIONING"] = ( + session_provisioning + ) + + @classmethod + def _read_only_provisioning( + cls, + request: interface.ConfigurationHookRequest, + resolved_entries: list[ResolvedSessionProvisioning], + init_environment: dict[str, t.Any], + environment: dict[str, t.Any], + ): + """Provisioning of read-only sessions""" + + init_environment["CAPELLACOLLAB_PROVISIONING"] = ( + cls._get_git_repos_json( + resolved_entries, + request.session_type, + include_credentials=True, + ) + ) + + environment["CAPELLACOLLAB_SESSION_PROVISIONING"] = ( + cls._get_git_repos_json( + resolved_entries, + request.session_type, + include_credentials=False, + ) ) @classmethod @@ -104,6 +229,21 @@ def _resolve_provisioning_request( ) return resolved_entries + @classmethod + def _verify_max_number_of_models( + cls, request: interface.ConfigurationHookRequest + ): + max_number_of_models = ( + request.tool.config.provisioning.max_number_of_models + ) + if ( + max_number_of_models + and len(request.provisioning) > max_number_of_models + ): + raise sessions_exceptions.TooManyModelsRequestedToProvisionError( + max_number_of_models + ) + @classmethod def _verify_matching_tool_version_and_model( cls, @@ -124,6 +264,19 @@ def _verify_matching_tool_version_and_model( model_name=entry["model"].name, ) + @classmethod + def _verify_matching_project_and_model( + cls, + project: projects_models.DatabaseProject, + resolved_entries: list[ResolvedSessionProvisioning], + ): + for entry in resolved_entries: + if entry["project"] != project: + raise sessions_exceptions.ProjectAndModelMismatchError( + project_slug=project.slug, + model_name=entry["model"].name, + ) + @classmethod def _verify_model_permissions( cls, @@ -143,14 +296,16 @@ def _verify_model_permissions( def _get_git_repos_json( cls, resolved_entries: list[ResolvedSessionProvisioning], + session_type: sessions_models.SessionType, include_credentials: bool = False, - ): + ) -> list[dict[str, str | int]]: """Get the git repos as a JSON-serializable list""" return [ cls._git_model_as_json( entry["git_model"], - entry["entry"].revision, + entry["entry"].revision or entry["git_model"].revision, entry["entry"].deep_clone, + session_type, include_credentials, ) for entry in resolved_entries @@ -162,6 +317,7 @@ def _git_model_as_json( git_model: git_models.DatabaseGitModel, revision: str, deep_clone: bool, + session_type: sessions_models.SessionType, include_credentials: bool, ) -> dict[str, str | int]: """Convert a DatabaseGitModel to a JSON-serializable dictionary.""" @@ -178,6 +334,8 @@ def _git_model_as_json( "path": str( pathlib.PurePosixPath( toolmodel.tool.config.provisioning.directory + if session_type == sessions_models.SessionType.READONLY + else "/workspace" ) / toolmodel.project.slug / toolmodel.slug @@ -187,3 +345,39 @@ def _git_model_as_json( git_dict["username"] = git_model.username git_dict["password"] = git_model.password return git_dict + + @classmethod + async def _determine_commit_hash( + cls, revision: str | None, git_model: git_models.DatabaseGitModel + ) -> tuple[str, str]: + revision = revision or git_model.revision + for hash, rev in await instances_git_core.ls_remote( + url=git_model.path, + username=git_model.username, + password=git_model.password, + ): + rev = rev.removeprefix("refs/heads/").removeprefix("refs/tags/") + if rev == revision: + return revision, hash + + raise instances_git_exceptions.RevisionNotFoundError(revision) + + @classmethod + async def _create_provisioning_record( + cls, + db: orm.Session, + resolved_entry: ResolvedSessionProvisioning, + user: users_models.DatabaseUser, + ) -> provisioning_models.DatabaseModelProvisioning: + rev, commit_hash = await cls._determine_commit_hash( + resolved_entry["entry"].revision, resolved_entry["git_model"] + ) + return provisioning_crud.create_model_provisioning( + db, + provisioning_models.DatabaseModelProvisioning( + user=user, + tool_model=resolved_entry["model"], + revision=rev, + commit_hash=commit_hash, + ), + ) diff --git a/backend/capellacollab/sessions/hooks/pure_variants.py b/backend/capellacollab/sessions/hooks/pure_variants.py index 75d8b8c19e..f0016ee620 100644 --- a/backend/capellacollab/sessions/hooks/pure_variants.py +++ b/backend/capellacollab/sessions/hooks/pure_variants.py @@ -5,8 +5,6 @@ import pathlib import typing as t -from sqlalchemy import orm - from capellacollab.core import models as core_models from capellacollab.projects.toolmodels import models as toolmodels_models from capellacollab.sessions import models as sessions_models @@ -26,20 +24,17 @@ class PureVariantsConfigEnvironment(t.TypedDict): class PureVariantsIntegration(interface.HookRegistration): - def configuration_hook( # type: ignore + def configuration_hook( self, - db: orm.Session, - user: users_models.DatabaseUser, - session_type: sessions_models.SessionType, - **kwargs, + request: interface.ConfigurationHookRequest, ) -> interface.ConfigurationHookResult: - if session_type == sessions_models.SessionType.READONLY: + if request.session_type == sessions_models.SessionType.READONLY: # Skip read-only sessions, no pure::variants integration supported. return interface.ConfigurationHookResult() if ( - not self._user_has_project_with_pure_variants_model(user) - and user.role == users_models.Role.USER + not self._user_has_project_with_pure_variants_model(request.user) + and request.user.role == users_models.Role.USER ): warnings = [ core_models.Message( @@ -57,7 +52,9 @@ def configuration_hook( # type: ignore warnings=warnings, ) - pv_license = purevariants_crud.get_pure_variants_configuration(db) + pv_license = purevariants_crud.get_pure_variants_configuration( + request.db + ) if not pv_license or pv_license.license_server_url is None: warnings = [ core_models.Message( diff --git a/backend/capellacollab/sessions/hooks/read_only_workspace.py b/backend/capellacollab/sessions/hooks/read_only_workspace.py index 10109a4812..cbe1b45119 100644 --- a/backend/capellacollab/sessions/hooks/read_only_workspace.py +++ b/backend/capellacollab/sessions/hooks/read_only_workspace.py @@ -12,12 +12,10 @@ class ReadOnlyWorkspaceHook(interface.HookRegistration): """Mounts an empty workspace to the container for read-only sessions.""" - def configuration_hook( # type: ignore - self, - session_type: sessions_models.SessionType, - **kwargs, + def configuration_hook( + self, request: interface.ConfigurationHookRequest ) -> interface.ConfigurationHookResult: - if session_type != sessions_models.SessionType.READONLY: + if request.session_type != sessions_models.SessionType.READONLY: # Configuration for persistent workspace sessions happens in the PersistentWorkspaceHook. return interface.ConfigurationHookResult() diff --git a/backend/capellacollab/sessions/hooks/session_preparation.py b/backend/capellacollab/sessions/hooks/session_preparation.py index 4e27ec7f4f..e81c7f53a7 100644 --- a/backend/capellacollab/sessions/hooks/session_preparation.py +++ b/backend/capellacollab/sessions/hooks/session_preparation.py @@ -2,39 +2,30 @@ # SPDX-License-Identifier: Apache-2.0 import pathlib -import typing as t from capellacollab.sessions import models as sessions_models from capellacollab.sessions.operators import models as operators_models -from capellacollab.tools import models as tools_models from . import interface -class PersistentWorkspacEnvironment(t.TypedDict): - pass - - class GitRepositoryCloningHook(interface.HookRegistration): """Creates a volume that is shared between the actual container and the session preparation. The volume is used to clone Git repositories as preparation for the session. """ - def configuration_hook( # type: ignore + def configuration_hook( self, - session_type: sessions_models.SessionType, - session_id: str, - tool: tools_models.DatabaseTool, - **kwargs, + request: interface.ConfigurationHookRequest, ) -> interface.ConfigurationHookResult: - if session_type != sessions_models.SessionType.READONLY: + if request.session_type != sessions_models.SessionType.READONLY: return interface.ConfigurationHookResult() shared_model_volume = operators_models.EmptyVolume( - name=f"{session_id}-models", + name=f"{request.session_id}-models", container_path=pathlib.PurePosixPath( - tool.config.provisioning.directory + request.tool.config.provisioning.directory ), read_only=False, ) diff --git a/backend/capellacollab/sessions/hooks/t4c.py b/backend/capellacollab/sessions/hooks/t4c.py index e6ffd4a884..92f03ba81e 100644 --- a/backend/capellacollab/sessions/hooks/t4c.py +++ b/backend/capellacollab/sessions/hooks/t4c.py @@ -17,7 +17,6 @@ from capellacollab.settings.modelsources.t4c.instance.repositories import ( interface as repo_interface, ) -from capellacollab.tools import models as tools_models from capellacollab.users import models as users_models from .. import models as sessions_models @@ -34,22 +33,19 @@ class T4CConfigEnvironment(t.TypedDict): class T4CIntegration(interface.HookRegistration): - def configuration_hook( # type: ignore - self, - db: orm.Session, - user: users_models.DatabaseUser, - tool_version: tools_models.DatabaseVersion, - session_type: sessions_models.SessionType, - **kwargs, + def configuration_hook( + self, request: interface.ConfigurationHookRequest ) -> interface.ConfigurationHookResult: - if session_type != sessions_models.SessionType.PERSISTENT: + user = request.user + + if request.session_type != sessions_models.SessionType.PERSISTENT: # Skip non-persistent sessions, no T4C integration needed. return interface.ConfigurationHookResult() warnings: list[core_models.Message] = [] t4c_repositories = repo_crud.get_user_t4c_repositories( - db, tool_version, user + request.db, request.tool_version, user ) t4c_json = json.dumps( @@ -91,7 +87,7 @@ def configuration_hook( # type: ignore password=environment["T4C_PASSWORD"], is_admin=auth_injectables.RoleVerification( required_role=users_models.Role.ADMIN, verify=False - )(user.name, db), + )(user.name, request.db), ) except requests.RequestException: warnings.append( @@ -116,30 +112,25 @@ def configuration_hook( # type: ignore environment=environment, warnings=warnings ) - def pre_session_termination_hook( # type: ignore - self, - db: orm.Session, - session: sessions_models.DatabaseSession, - **kwargs, + def pre_session_termination_hook( + self, request: interface.PreSessionTerminationHookRequest ): - if session.type == sessions_models.SessionType.PERSISTENT: - self._revoke_session_tokens(db, session) + if request.session.type == sessions_models.SessionType.PERSISTENT: + self._revoke_session_tokens(request.db, request.session) - def session_connection_hook( # type: ignore[override] + def session_connection_hook( self, - db_session: sessions_models.DatabaseSession, - user: users_models.DatabaseUser, - **kwargs, + request: interface.SessionConnectionHookRequest, ) -> interface.SessionConnectionHookResult: - if db_session.type != sessions_models.SessionType.PERSISTENT: + if request.db_session.type != sessions_models.SessionType.PERSISTENT: return interface.SessionConnectionHookResult() - if db_session.owner != user: + if request.db_session.owner != request.user: # The session is shared, don't provide the T4C token. return interface.SessionConnectionHookResult() return interface.SessionConnectionHookResult( - t4c_token=db_session.environment.get("T4C_PASSWORD") + t4c_token=request.db_session.environment.get("T4C_PASSWORD") ) def _revoke_session_tokens( diff --git a/backend/capellacollab/sessions/idletimeout.py b/backend/capellacollab/sessions/idletimeout.py index f3a410628d..da7953cf41 100644 --- a/backend/capellacollab/sessions/idletimeout.py +++ b/backend/capellacollab/sessions/idletimeout.py @@ -18,6 +18,7 @@ def terminate_idle_session(): + log.debug("Starting to terminate idle sessions...") url = config.prometheus.url url += "/".join(("api", "v1", 'query?query=ALERTS{alertstate="firing"}')) response = requests.get( @@ -42,6 +43,7 @@ def terminate_idle_session(): session_id, ) operators.get_operator().kill_session(session_id) + log.debug("Finished termination of idle sessions.") async def terminate_idle_sessions_in_background(interval=60): diff --git a/backend/capellacollab/sessions/injection.py b/backend/capellacollab/sessions/injection.py index 4526a337a5..c0f52a7a52 100644 --- a/backend/capellacollab/sessions/injection.py +++ b/backend/capellacollab/sessions/injection.py @@ -19,6 +19,8 @@ def get_last_seen(sid: str) -> str: if core.LOCAL_DEVELOPMENT_MODE: return "Disabled in development mode" + log.debug("Starting to get last seen for session %s.", sid) + url = f"{config.prometheus.url}/api/v1/query?query=idletime_minutes" try: response = requests.get( @@ -29,7 +31,11 @@ def get_last_seen(sid: str) -> str: for session in response.json()["data"]["result"]: if sid == session["metric"]["session_id"]: - return _get_last_seen(float(session["value"][1])) + last_seen = _get_last_seen(float(session["value"][1])) + log.debug( + "Returning last seen %s for session %s.", last_seen, sid + ) + return last_seen log.debug("Couldn't find Prometheus metrics for session %s.", sid) except Exception: @@ -48,17 +54,34 @@ def _get_last_seen(idletime: int | float) -> str: def determine_session_state(session_id: str) -> str: + log.debug( + "Starting fetching the session state for session %s.", session_id + ) state = operators.get_operator().get_session_state(session_id) if state in ("Started", "BackOff"): try: + log.debug("Fetching session logs for session %s.", session_id) logs = operators.get_operator().get_session_logs( session_id, container="session-preparation" ) logs += operators.get_operator().get_session_logs(session_id) + log.debug( + "Evaluating regular expression to find status in logs for session %s.", + session_id, + ) res = re.search(r"(?s:.*)^---(.*?)---$", logs, re.MULTILINE) if res: - return res.group(1) + result = res.group(1) + log.debug( + "Found result '%s' in session logs for session %s.", + result, + session_id, + ) + return result except Exception: log.exception("Could not parse log") + log.debug( + "Returning session state '%s' for session %s.", state, session_id + ) return state diff --git a/backend/capellacollab/sessions/models.py b/backend/capellacollab/sessions/models.py index 566811145e..2881e26a28 100644 --- a/backend/capellacollab/sessions/models.py +++ b/backend/capellacollab/sessions/models.py @@ -21,6 +21,9 @@ from . import injection if t.TYPE_CHECKING: + from capellacollab.projects.toolmodels.provisioning.models import ( + DatabaseModelProvisioning, + ) from capellacollab.tools.models import DatabaseTool, DatabaseVersion from capellacollab.users.models import DatabaseUser @@ -49,7 +52,7 @@ class SessionProvisioningRequest(core_pydantic.BaseModel): project_slug: str toolmodel_slug: str = pydantic.Field(alias="model_slug") git_model_id: int - revision: str + revision: str | None = None deep_clone: bool @@ -58,10 +61,22 @@ class PostSessionRequest(core_pydantic.BaseModel): version_id: int session_type: SessionType = pydantic.Field(default=SessionType.PERSISTENT) - connection_method_id: str = pydantic.Field( - description="The identifier of the connection method to use" + connection_method_id: str | None = pydantic.Field( + default=None, + description=( + "The identifier of the connection method to use." + " If None, the default connection method will be used." + ), ) provisioning: list[SessionProvisioningRequest] = pydantic.Field(default=[]) + project_slug: str | None = pydantic.Field( + default=None, + description=( + "The project to run the session in." + " Required for persistent provisioned sessions." + " Ignored for readonly sessions." + ), + ) class SessionSharing(core_pydantic.BaseModel): @@ -158,6 +173,18 @@ class DatabaseSession(database.Base): connection_method_id: orm.Mapped[str] + provisioning_id: orm.Mapped[int | None] = orm.mapped_column( + sa.ForeignKey("model_provisioning.id"), + init=False, + ) + provisioning: orm.Mapped[DatabaseModelProvisioning | None] = ( + orm.relationship( + back_populates="session", + foreign_keys=[provisioning_id], + default=None, + ) + ) + environment: orm.Mapped[dict[str, str]] = orm.mapped_column( nullable=False, default_factory=dict ) diff --git a/backend/capellacollab/sessions/operators/k8s.py b/backend/capellacollab/sessions/operators/k8s.py index bf733fc392..88eeb4fd93 100644 --- a/backend/capellacollab/sessions/operators/k8s.py +++ b/backend/capellacollab/sessions/operators/k8s.py @@ -19,6 +19,7 @@ import kubernetes.config import kubernetes.stream.stream import prometheus_client +import typing_extensions as te # codespell:ignore te import yaml from kubernetes import client from kubernetes.client import exceptions @@ -53,7 +54,7 @@ ) -class Session(t.TypedDict): +class Session(te.TypedDict): # codespell:ignore te id: str port: int created_at: datetime.datetime @@ -205,6 +206,10 @@ def _get_pod_state(self, label_selector: str) -> str: ) events = list(filter(self._is_non_promtail_event, events)) + + log.debug( + "Found %d matching events for pod: %s", len(events), pod_name + ) if events: return events[-1].reason diff --git a/backend/capellacollab/sessions/routes.py b/backend/capellacollab/sessions/routes.py index 7ee7908701..c8c622e2c4 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -17,8 +17,11 @@ from capellacollab.core import responses from capellacollab.core.authentication import exceptions as auth_exceptions from capellacollab.core.authentication import injectables as auth_injectables -from capellacollab.sessions import hooks +from capellacollab.projects import injectables as projects_injectables +from capellacollab.projects.users import models as projects_users_models +from capellacollab.sessions import hooks as sessions_hooks from capellacollab.sessions.files import routes as files_routes +from capellacollab.sessions.hooks import interface as hooks_interface from capellacollab.tools import exceptions as tools_exceptions from capellacollab.tools import injectables as tools_injectables from capellacollab.tools import models as tools_models @@ -82,7 +85,7 @@ ], ), ) -def request_session( +async def request_session( body: models.PostSessionRequest, user: users_models.DatabaseUser = fastapi.Depends( users_injectables.get_own_user @@ -95,27 +98,31 @@ def request_session( "Starting %s session for user %s", body.session_type, user.name ) - # Provisioning will be supported in the future: - # https://github.com/DSD-DBS/capella-collab-manager/issues/1004 - if ( - body.session_type == models.SessionType.PERSISTENT - and body.provisioning - ): - raise exceptions.ProvisioningUnsupportedError() - tool = tools_injectables.get_existing_tool(body.tool_id, db) version = tools_injectables.get_existing_tool_version( tool.id, body.version_id, db ) - connection_method: tools_models.ToolSessionConnectionMethod = ( - util.get_connection_method(tool, body.connection_method_id) - ) + if body.connection_method_id: + connection_method: tools_models.ToolSessionConnectionMethod = ( + util.get_connection_method(tool, body.connection_method_id) + ) + else: + connection_method = tool.config.connection.methods[0] session_id = util.generate_id() util.raise_if_conflicting_sessions(tool, version, body.session_type, user) + project_scope = None + if body.project_slug: + auth_injectables.ProjectRoleVerification( + required_role=projects_users_models.ProjectUserRole.USER + )(body.project_slug, user.name, db) + project_scope = projects_injectables.get_existing_project( + body.project_slug, db + ) + environment = t.cast( dict[str, str], util.get_environment(user, connection_method, session_id), @@ -125,19 +132,22 @@ def request_session( init_volumes: list[operators_models.Volume] = [] init_environment: dict[str, str] = {} - for hook in hooks.get_activated_integration_hooks(tool): - hook_result = hook.configuration_hook( - db=db, - user=user, - tool_version=version, - tool=tool, - username=user.name, - operator=operator, - session_type=body.session_type, - connection_method=connection_method, - provisioning=body.provisioning, - session_id=session_id, - ) + hook_request = hooks_interface.ConfigurationHookRequest( + db=db, + user=user, + tool_version=version, + tool=tool, + operator=operator, + session_type=body.session_type, + connection_method=connection_method, + provisioning=body.provisioning, + session_id=session_id, + project_scope=project_scope, + ) + + for hook_result in await util.schedule_configuration_hooks( + hook_request, tool + ): environment |= hook_result.get("environment", {}) init_environment |= hook_result.get("init_environment", {}) volumes += hook_result.get("volumes", []) @@ -222,14 +232,16 @@ def request_session( ) hook_config: dict[str, str] = {} - for hook in hooks.get_activated_integration_hooks(tool): + for hook in sessions_hooks.get_activated_integration_hooks(tool): result = hook.post_session_creation_hook( - session_id=session_id, - operator=operator, - user=user, - session=session, - db_session=db_session, - connection_method=connection_method, + hooks_interface.PostSessionCreationHookRequest( + session_id=session_id, + operator=operator, + user=user, + session=session, + db_session=db_session, + connection_method=connection_method, + ) ) hook_config |= result.get("config", {}) @@ -365,13 +377,15 @@ def get_session_connection_information( redirect_url = None t4c_token = None - for hook in hooks.get_activated_integration_hooks(session.tool): + for hook in sessions_hooks.get_activated_integration_hooks(session.tool): hook_result = hook.session_connection_hook( - db=db, - user=user, - db_session=session, - connection_method=connection_method, - logger=logger, + hooks_interface.SessionConnectionHookRequest( + db=db, + db_session=session, + connection_method=connection_method, + logger=logger, + user=user, + ) ) local_storage |= hook_result.get("local_storage", {}) diff --git a/backend/capellacollab/sessions/util.py b/backend/capellacollab/sessions/util.py index 8abe6876f0..d1170d63fa 100644 --- a/backend/capellacollab/sessions/util.py +++ b/backend/capellacollab/sessions/util.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 +import asyncio import json import logging import random @@ -12,7 +13,7 @@ from capellacollab.config import config from capellacollab.core import credentials from capellacollab.core import models as core_models -from capellacollab.sessions import hooks +from capellacollab.sessions.hooks import interface as hooks_interface from capellacollab.sessions.operators import k8s from capellacollab.tools import models as tools_models from capellacollab.users import models as users_models @@ -33,11 +34,12 @@ def terminate_session( ) for hook in hooks.get_activated_integration_hooks(session.tool): hook.pre_session_termination_hook( - db=db, - session=session, - operator=operator, - user=session.owner, - connection_method=connection_method, + hooks_interface.PreSessionTerminationHookRequest( + db=db, + session=session, + operator=operator, + connection_method=connection_method, + ) ) crud.delete_session(db, session) @@ -202,3 +204,24 @@ def is_session_shared_with_user( session: models.DatabaseSession, user: users_models.DatabaseUser ) -> bool: return user in [shared.user for shared in session.shared_with] + + +async def schedule_configuration_hooks( + request: hooks_interface.ConfigurationHookRequest, + tool: tools_models.DatabaseTool, +) -> list[hooks_interface.ConfigurationHookResult]: + """Schedule sync and async configuration hooks + + Schedule async hooks, then schedule the sync hooks + and finally collect the async hook results + """ + + activated_hooks = hooks.get_activated_integration_hooks(tool) + + async_hooks = [ + hook.async_configuration_hook(request) for hook in activated_hooks + ] + + return [ + hook.configuration_hook(request) for hook in activated_hooks + ] + await asyncio.gather(*async_hooks) diff --git a/backend/capellacollab/settings/modelsources/git/core.py b/backend/capellacollab/settings/modelsources/git/core.py index ea23924ffa..6863059cb8 100644 --- a/backend/capellacollab/settings/modelsources/git/core.py +++ b/backend/capellacollab/settings/modelsources/git/core.py @@ -2,18 +2,19 @@ # SPDX-License-Identifier: Apache-2.0 import asyncio -import collections.abc as cabc import logging import os import pathlib import subprocess +import typing as t from . import exceptions, models log = logging.getLogger(__name__) -async def ls_remote(url: str, env: cabc.Mapping[str, str]) -> list[str]: +async def _ls_remote_command(url: str, env: t.Mapping[str, str]) -> str: + """Runs ls-remote on a repository and returns stdout""" try: proc = await asyncio.create_subprocess_exec( "git", @@ -36,8 +37,30 @@ async def ls_remote(url: str, env: cabc.Mapping[str, str]) -> list[str]: raise exceptions.GitRepositoryAccessError() else: raise e + stdout, _ = await proc.communicate() - return stdout.decode().strip().splitlines() + return stdout.decode() + + +async def ls_remote( + url: str, username: str | None, password: str | None +) -> list[tuple[str, str]]: + env = { + "GIT_USERNAME": username or "", + "GIT_PASSWORD": password or "", + "GIT_ASKPASS": str( + (pathlib.Path(__file__).parents[0] / "askpass.py").absolute() + ), + } + + stdout = await _ls_remote_command(url, env) + + resolved_revisions = [] + for line in stdout.strip().splitlines(): + (hash, rev) = line.split("\t") + resolved_revisions.append((hash, rev)) + + return resolved_revisions async def get_remote_refs( @@ -47,15 +70,7 @@ async def get_remote_refs( models.GetRevisionsResponseModel(branches=[], tags=[]) ) - git_env = { - "GIT_USERNAME": username or "", - "GIT_PASSWORD": password or "", - "GIT_ASKPASS": str( - (pathlib.Path(__file__).parents[0] / "askpass.py").absolute() - ), - } - for ref in await ls_remote(url, git_env): - (_, ref) = ref.split("\t") + for _, ref in await ls_remote(url, username, password): if "^" in ref: continue if ref.startswith("refs/heads/"): diff --git a/backend/capellacollab/settings/modelsources/git/exceptions.py b/backend/capellacollab/settings/modelsources/git/exceptions.py index dcf656a6aa..4d8df820e7 100644 --- a/backend/capellacollab/settings/modelsources/git/exceptions.py +++ b/backend/capellacollab/settings/modelsources/git/exceptions.py @@ -41,3 +41,13 @@ def __init__(self): ), err_code="NO_GIT_INSTANCE_WITH_PREFIX_FOUND", ) + + +class RevisionNotFoundError(core_exceptions.BaseError): + def __init__(self, revision: str): + super().__init__( + status_code=status.HTTP_404_NOT_FOUND, + title="Revision not found in repository", + reason=f"The revision '{revision}' is not a valid branch or tag name.", + err_code="GIT_REVISION_NOT_FOUND", + ) diff --git a/backend/capellacollab/settings/modelsources/git/routes.py b/backend/capellacollab/settings/modelsources/git/routes.py index 2d6bc53cc6..9c11b9fea0 100644 --- a/backend/capellacollab/settings/modelsources/git/routes.py +++ b/backend/capellacollab/settings/modelsources/git/routes.py @@ -8,7 +8,7 @@ from capellacollab.core import database from capellacollab.core.authentication import injectables as auth_injectables -from capellacollab.settings.modelsources.git import core as git_core +from capellacollab.settings.modelsources.git import core as instances_git_core from capellacollab.users import models as users_models from . import crud, injectables, models, util @@ -111,7 +111,7 @@ async def get_revisions( username = body.credentials.username password = body.credentials.password - return await git_core.get_remote_refs(url, username, password) + return await instances_git_core.get_remote_refs(url, username, password) @router.post("/validate/path", response_model=bool) diff --git a/backend/capellacollab/tools/models.py b/backend/capellacollab/tools/models.py index 33116c077c..ff25924171 100644 --- a/backend/capellacollab/tools/models.py +++ b/backend/capellacollab/tools/models.py @@ -270,6 +270,13 @@ class ToolModelProvisioning(core_pydantic.BaseModel): ), examples=[None, 1], ) + required: bool = pydantic.Field( + default=False, + description=( + "Specifies if a tool requires provisioning." + " If enabled and a session without provisioning is requested, it will be declined." + ), + ) class PersistentWorkspaceSessionConfiguration(core_pydantic.BaseModel): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index dcf2abc8f5..83d8802a7f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -72,6 +72,7 @@ dev = [ "types-lxml", "types-croniter", "pyinstrument", + "pytest-asyncio", ] [tool.black] diff --git a/backend/tests/core/test_auth_injectables.py b/backend/tests/core/test_auth_injectables.py index e8d22fa567..940db88848 100644 --- a/backend/tests/core/test_auth_injectables.py +++ b/backend/tests/core/test_auth_injectables.py @@ -18,13 +18,6 @@ def fixture_verify(request: pytest.FixtureRequest) -> bool: return request.param -@pytest.fixture(name="user2") -def fixture_user2(db: orm.Session) -> users_models.DatabaseUser: - return users_crud.create_user( - db, "user2", "user2", None, users_models.Role.USER - ) - - @pytest.fixture(name="admin2") def fixture_admin2(db: orm.Session) -> users_models.DatabaseUser: return users_crud.create_user( diff --git a/backend/tests/projects/test_projects_tools.py b/backend/tests/projects/test_projects_tools.py new file mode 100644 index 0000000000..c206426ed4 --- /dev/null +++ b/backend/tests/projects/test_projects_tools.py @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from fastapi import testclient +from sqlalchemy import orm + +from capellacollab.projects import models as projects_models +from capellacollab.projects.tools import crud as projects_tools_crud +from capellacollab.projects.tools import models as projects_tools_models +from capellacollab.tools import models as tools_models + + +@pytest.fixture(name="project_tool") +def fixture_jupyter_project_tool( + db: orm.Session, + project: projects_models.DatabaseProject, + tool_version: tools_models.DatabaseVersion, +) -> projects_tools_models.DatabaseProjectToolAssociation: + return projects_tools_crud.create_project_tool(db, project, tool_version) + + +@pytest.mark.usefixtures("capella_model", "project_tool", "project_user") +def test_get_project_tools( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_tool_version: tools_models.DatabaseVersion, + tool_version: tools_models.DatabaseVersion, +): + """Test to get all tools of a project + + Explicitly test that manually added tools + and auto-added tools are listed. + """ + + response = client.get(f"/api/v1/projects/{project.slug}/tools") + + assert response.status_code == 200 + json = response.json() + + assert len(json) == 2 + assert json[0]["tool_version"]["id"] == tool_version.id + assert json[1]["tool_version"]["id"] == capella_tool_version.id + assert len(json[1]["used_by"]) == 1 + + +@pytest.mark.usefixtures("project_manager") +def test_link_tool_to_project( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_tool_version: tools_models.DatabaseVersion, +): + """Test to link a tool to a project""" + + response = client.post( + f"/api/v1/projects/{project.slug}/tools", + json={ + "tool_version_id": capella_tool_version.id, + "tool_id": capella_tool_version.tool.id, + }, + ) + + assert response.status_code == 200 + assert response.json()["tool_version"]["id"] == capella_tool_version.id + + +@pytest.mark.usefixtures("project_tool", "project_manager") +def test_link_tool_to_project_already_linked( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + tool_version: tools_models.DatabaseVersion, +): + """Test to link a tool to a project that is already linked""" + response = client.post( + f"/api/v1/projects/{project.slug}/tools", + json={ + "tool_version_id": tool_version.id, + "tool_id": tool_version.tool.id, + }, + ) + + assert response.status_code == 409 + assert ( + response.json()["detail"]["err_code"] + == "TOOL_ALREADY_EXISTS_IN_PROJECT" + ) + + +@pytest.mark.usefixtures("project_manager") +def test_remove_tool_from_project( + db: orm.Session, + client: testclient.TestClient, + project: projects_models.DatabaseProject, + project_tool: projects_tools_models.DatabaseProjectToolAssociation, +): + """Test to remove a tool to a project""" + + response = client.delete( + f"/api/v1/projects/{project.slug}/tools/{project_tool.id}" + ) + + assert response.status_code == 204 + assert ( + projects_tools_crud.get_project_tool_by_id(db, project_tool.id) is None + ) + + +@pytest.mark.usefixtures("project_manager") +def test_remove_non_existing_tool_from_project( + db: orm.Session, + client: testclient.TestClient, + project: projects_models.DatabaseProject, +): + """Test to remove a non-existing tools to a project""" + + response = client.delete(f"/api/v1/projects/{project.slug}/tools/0") + + assert response.status_code == 404 + assert response.json()["detail"]["err_code"] == "PROJECT_TOOL_NOT_FOUND" diff --git a/backend/tests/projects/test_projects_users_routes.py b/backend/tests/projects/test_projects_users_routes.py index 5ddfe775b3..04979e7546 100644 --- a/backend/tests/projects/test_projects_users_routes.py +++ b/backend/tests/projects/test_projects_users_routes.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 +import pytest from fastapi import testclient from sqlalchemy import orm @@ -12,32 +13,25 @@ from capellacollab.users import models as users_models +@pytest.mark.usefixtures("admin") def test_assign_read_write_permission_when_adding_manager( db: orm.Session, client: testclient.TestClient, - executor_name: str, - unique_username: str, + user2: users_models.DatabaseUser, project: projects_models.DatabaseProject, ): - users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - response = client.post( f"/api/v1/projects/{project.slug}/users/", json={ "role": projects_users_models.ProjectUserRole.MANAGER.value, "permission": projects_users_models.ProjectUserPermission.READ.value, - "username": user.name, + "username": user2.name, "reason": "", }, ) project_user = projects_users_crud.get_project_user_association( - db, project, user + db, project, user2 ) assert response.status_code == 200 @@ -49,30 +43,23 @@ def test_assign_read_write_permission_when_adding_manager( ) +@pytest.mark.usefixtures("admin") def test_assign_read_write_permission_when_changing_project_role_to_manager( db: orm.Session, client: testclient.TestClient, - executor_name: str, - unique_username: str, project: projects_models.DatabaseProject, + user2: users_models.DatabaseUser, ): - users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - projects_users_crud.add_user_to_project( db, project, - user, + user2, projects_users_models.ProjectUserRole.USER, projects_users_models.ProjectUserPermission.READ, ) response = client.patch( - f"/api/v1/projects/{project.slug}/users/{user.id}", + f"/api/v1/projects/{project.slug}/users/{user2.id}", json={ "role": projects_users_models.ProjectUserRole.MANAGER.value, "reason": "", @@ -80,7 +67,7 @@ def test_assign_read_write_permission_when_changing_project_role_to_manager( ) project_user = projects_users_crud.get_project_user_association( - db, project, user + db, project, user2 ) assert response.status_code == 204 @@ -92,30 +79,23 @@ def test_assign_read_write_permission_when_changing_project_role_to_manager( ) +@pytest.mark.usefixtures("admin") def test_http_exception_when_updating_permission_of_manager( db: orm.Session, client: testclient.TestClient, - executor_name: str, - unique_username: str, + user2: users_models.DatabaseUser, project: projects_models.DatabaseProject, ): - users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - projects_users_crud.add_user_to_project( db, project, - user, + user2, projects_users_models.ProjectUserRole.MANAGER, projects_users_models.ProjectUserPermission.WRITE, ) response = client.patch( - f"/api/v1/projects/{project.slug}/users/{user.id}", + f"/api/v1/projects/{project.slug}/users/{user2.id}", json={ "permission": projects_users_models.ProjectUserPermission.READ.value, "reason": "", diff --git a/backend/tests/projects/toolmodels/provisioning/fixtures.py b/backend/tests/projects/toolmodels/provisioning/fixtures.py new file mode 100644 index 0000000000..f31a2d3cb8 --- /dev/null +++ b/backend/tests/projects/toolmodels/provisioning/fixtures.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import datetime + +import pytest +from sqlalchemy import orm + +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.toolmodels.provisioning import ( + crud as provisioning_crud, +) +from capellacollab.projects.toolmodels.provisioning import ( + models as provisioning_models, +) +from capellacollab.users import models as users_models + + +@pytest.fixture(name="provisioning") +def fixture_provisioning( + db: orm.Session, + user: users_models.DatabaseUser, + capella_model: toolmodels_models.DatabaseToolModel, +): + return provisioning_crud.create_model_provisioning( + db, + provisioning_models.DatabaseModelProvisioning( + user=user, + tool_model=capella_model, + revision="main", + commit_hash="db45166576e7f1e7fec3256e8657ba431f9b5b77", + provisioned_at=datetime.datetime.now(), + session=None, + ), + ) diff --git a/backend/tests/projects/toolmodels/provisioning/test_provisioning.py b/backend/tests/projects/toolmodels/provisioning/test_provisioning.py new file mode 100644 index 0000000000..4c90a50cc7 --- /dev/null +++ b/backend/tests/projects/toolmodels/provisioning/test_provisioning.py @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from fastapi import testclient +from sqlalchemy import orm + +from capellacollab.projects import models as projects_models +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.toolmodels.provisioning import ( + crud as provisioning_crud, +) +from capellacollab.projects.toolmodels.provisioning import ( + models as provisioning_models, +) +from capellacollab.users import models as users_models + + +@pytest.mark.usefixtures("project_user") +def test_get_non_existing_provisioning( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, +): + """Test that a non-existing provisioning returns None""" + + response = client.get( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/provisioning" + ) + + assert response.status_code == 200 + assert response.json() is None + + +@pytest.mark.usefixtures("project_user") +def test_get_provisioning( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + provisioning: provisioning_models.DatabaseModelProvisioning, +): + """Test to get an existing provisioning""" + response = client.get( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/provisioning" + ) + + assert response.status_code == 200 + + assert response.json() is not None + assert response.json()["commit_hash"] == provisioning.commit_hash + + +@pytest.mark.usefixtures("project_user", "provisioning") +def test_delete_provisioning( + db: orm.Session, + user: users_models.DatabaseUser, + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, +): + """Test to delete an existing provisioning""" + response = client.delete( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/provisioning" + ) + + assert response.status_code == 204 + assert ( + provisioning_crud.get_model_provisioning(db, capella_model, user) + is None + ) + + +@pytest.mark.usefixtures("project_user") +def test_delete_non_existing_provisioning( + db: orm.Session, + user: users_models.DatabaseUser, + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, +): + """Test to delete an non-existing provisioning""" + + response = client.delete( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/provisioning" + ) + + assert response.status_code == 404 + assert response.json()["detail"]["err_code"] == "PROVISIONING_NOT_FOUND" diff --git a/backend/tests/sessions/hooks/conftest.py b/backend/tests/sessions/hooks/conftest.py new file mode 100644 index 0000000000..1dfb229165 --- /dev/null +++ b/backend/tests/sessions/hooks/conftest.py @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import datetime +import logging + +import pytest +from sqlalchemy import orm + +from capellacollab.sessions import models as sessions_models +from capellacollab.sessions.hooks import interface as hooks_interface +from capellacollab.sessions.operators import k8s as k8s_operator +from capellacollab.tools import models as tools_models +from capellacollab.users import models as users_models + + +@pytest.fixture(name="configuration_hook_request") +def fixture_configuration_hook_request( + db: orm.Session, + user: users_models.DatabaseUser, + capella_tool: tools_models.DatabaseTool, + capella_tool_version: tools_models.DatabaseVersion, +) -> hooks_interface.ConfigurationHookRequest: + return hooks_interface.ConfigurationHookRequest( + db=db, + operator=k8s_operator.KubernetesOperator(), + user=user, + tool=capella_tool, + tool_version=capella_tool_version, + session_type=sessions_models.SessionType.PERSISTENT, + connection_method=tools_models.GuacamoleConnectionMethod(), + provisioning=[], + session_id="nxylxqbmfqwvswlqlcbsirvrt", + project_scope=None, + ) + + +@pytest.fixture(name="post_session_creation_hook_request") +def fixture_post_session_creation_hook_request( + session: sessions_models.DatabaseSession, + user: users_models.DatabaseUser, +) -> hooks_interface.PostSessionCreationHookRequest: + return hooks_interface.PostSessionCreationHookRequest( + session_id="test", + db_session=session, + session={ + "id": "test", + "port": 8080, + "created_at": datetime.datetime.fromisoformat( + "2021-01-01T00:00:00" + ), + "host": "test", + }, + user=user, + connection_method=tools_models.GuacamoleConnectionMethod(), + operator=k8s_operator.KubernetesOperator(), + ) + + +@pytest.fixture(name="session_connection_hook_request") +def fixture_session_connection_hook_request( + db: orm.Session, + user: users_models.DatabaseUser, + session: sessions_models.DatabaseSession, + logger: logging.LoggerAdapter, +) -> hooks_interface.SessionConnectionHookRequest: + + return hooks_interface.SessionConnectionHookRequest( + db=db, + db_session=session, + connection_method=tools_models.GuacamoleConnectionMethod(), + logger=logger, + user=user, + ) + + +@pytest.fixture(name="pre_session_termination_hook_request") +def fixture_pre_session_termination_hook_request( + db: orm.Session, + session: sessions_models.DatabaseSession, +) -> hooks_interface.PreSessionTerminationHookRequest: + return hooks_interface.PreSessionTerminationHookRequest( + db=db, + connection_method=tools_models.GuacamoleConnectionMethod(), + operator=k8s_operator.KubernetesOperator(), + session=session, + ) diff --git a/backend/tests/sessions/hooks/test_guacamole_hook.py b/backend/tests/sessions/hooks/test_guacamole_hook.py index 3a6013cfee..c79dece563 100644 --- a/backend/tests/sessions/hooks/test_guacamole_hook.py +++ b/backend/tests/sessions/hooks/test_guacamole_hook.py @@ -127,23 +127,12 @@ def match_user_creation_body( "guacamole_create_token", "guacamole_delete_user", "guacamole_apis" ) def test_guacamole_configuration_hook( - session: sessions_models.DatabaseSession, + post_session_creation_hook_request: session_hooks_interface.PostSessionCreationHookRequest, ): """Test that the Guacamole hook creates a user and a connection""" response = guacamole.GuacamoleIntegration().post_session_creation_hook( - session_id="test", - db_session=session, - session={ - "id": "test", - "port": "8080", - "created_at": "2021-01-01T00:00:00", - "host": "test", - }, - user=users_models.DatabaseUser( - name="test", idp_identifier="test", role=users_models.Role.USER - ), - connection_method=tools_models.GuacamoleConnectionMethod(), + post_session_creation_hook_request ) assert response["config"]["guacamole_username"] @@ -157,7 +146,7 @@ def test_guacamole_configuration_hook( "guacamole_create_token", "guacamole_delete_user", "guacamole_apis" ) def test_fail_if_guacamole_unreachable( - session: sessions_models.DatabaseSession, + post_session_creation_hook_request: session_hooks_interface.PostSessionCreationHookRequest, ): """If Guacamole is unreachable, the session hook will abort the session creation""" @@ -171,18 +160,7 @@ def test_fail_if_guacamole_unreachable( with pytest.raises(guacamole.GuacamoleError): guacamole.GuacamoleIntegration().post_session_creation_hook( - session_id="test", - db_session=session, - session={ - "id": "test", - "port": "8080", - "created_at": "2021-01-01T00:00:00", - "host": "test", - }, - user=users_models.DatabaseUser( - name="test", idp_identifier="test", role=users_models.Role.USER - ), - connection_method=tools_models.GuacamoleConnectionMethod(), + post_session_creation_hook_request ) @@ -191,26 +169,18 @@ def test_fail_if_guacamole_unreachable( "guacamole_create_token", "guacamole_delete_user", "guacamole_apis" ) def test_guacamole_hook_not_executed_for_http_method( - session: sessions_models.DatabaseSession, + post_session_creation_hook_request: session_hooks_interface.PostSessionCreationHookRequest, ): """Skip if connection method is not Guacamole If the connection method is not Guacamole, the hook should skip the preparation. """ + post_session_creation_hook_request.connection_method = ( + tools_models.HTTPConnectionMethod() + ) response = guacamole.GuacamoleIntegration().post_session_creation_hook( - session_id="test", - session={ - "id": "test", - "port": "8080", - "created_at": "2021-01-01T00:00:00", - "host": "test", - }, - db_session=session, - user=users_models.DatabaseUser( - name="test", idp_identifier="test", role=users_models.Role.USER - ), - connection_method=tools_models.HTTPConnectionMethod(), + post_session_creation_hook_request ) assert session_hooks_interface.PostSessionCreationHookResult() == response @@ -222,23 +192,12 @@ def test_guacamole_hook_not_executed_for_http_method( "guacamole_create_token", "guacamole_delete_user", "guacamole_apis" ) def test_skip_guacamole_user_deletion_on_404( - session: sessions_models.DatabaseSession, + post_session_creation_hook_request: session_hooks_interface.PostSessionCreationHookRequest, ): """If the user does not exist, the hook should not fail""" response = guacamole.GuacamoleIntegration().post_session_creation_hook( - session_id="test", - db_session=session, - session={ - "id": "test", - "port": "8080", - "created_at": "2021-01-01T00:00:00", - "host": "test", - }, - user=users_models.DatabaseUser( - name="test", idp_identifier="test", role=users_models.Role.USER - ), - connection_method=tools_models.GuacamoleConnectionMethod(), + post_session_creation_hook_request ) assert response["config"] diff --git a/backend/tests/sessions/hooks/test_http_hook.py b/backend/tests/sessions/hooks/test_http_hook.py index 79acf10ed3..1fc65cc55a 100644 --- a/backend/tests/sessions/hooks/test_http_hook.py +++ b/backend/tests/sessions/hooks/test_http_hook.py @@ -1,19 +1,15 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import logging - from capellacollab.sessions import models as sessions_models from capellacollab.sessions.hooks import http from capellacollab.sessions.hooks import interface as sessions_hooks_interface from capellacollab.tools import models as tools_models -from capellacollab.users import models as users_models def test_http_hook( session: sessions_models.DatabaseSession, - user: users_models.DatabaseUser, - logger: logging.LoggerAdapter, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, ): session.environment = { "TEST": "test", @@ -23,11 +19,10 @@ def test_http_hook( redirect_url="http://localhost:8000/{TEST}", cookies={"test": "{TEST}"}, ) + session_connection_hook_request.connection_method = connection_method + session_connection_hook_request.db_session = session result = http.HTTPIntegration().session_connection_hook( - db_session=session, - user=user, - connection_method=connection_method, - logger=logger, + session_connection_hook_request ) assert result["cookies"]["test"] == "test" @@ -36,33 +31,26 @@ def test_http_hook( def test_skip_http_hook_if_guacamole( - session: sessions_models.DatabaseSession, - user: users_models.DatabaseUser, - logger: logging.LoggerAdapter, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, ): result = http.HTTPIntegration().session_connection_hook( - db_session=session, - connection_method=tools_models.GuacamoleConnectionMethod(), - user=user, - logger=logger, + session_connection_hook_request ) assert result == sessions_hooks_interface.SessionConnectionHookResult() def test_fail_derive_redirect_url( session: sessions_models.DatabaseSession, - user: users_models.DatabaseUser, - logger: logging.LoggerAdapter, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, ): session.environment = {"TEST": "test"} connection_method = tools_models.HTTPConnectionMethod( redirect_url="http://localhost:8000/{TEST2}" ) + session_connection_hook_request.connection_method = connection_method + session_connection_hook_request.db_session = session result = http.HTTPIntegration().session_connection_hook( - db_session=session, - connection_method=connection_method, - user=user, - logger=logger, + session_connection_hook_request ) assert len(result["warnings"]) == 1 diff --git a/backend/tests/sessions/hooks/test_jupyter_hook.py b/backend/tests/sessions/hooks/test_jupyter_hook.py index a4333561bb..8c9a36817b 100644 --- a/backend/tests/sessions/hooks/test_jupyter_hook.py +++ b/backend/tests/sessions/hooks/test_jupyter_hook.py @@ -3,34 +3,40 @@ import pytest -from sqlalchemy import orm import capellacollab.projects.toolmodels.models as toolmodels_models +from capellacollab.sessions.hooks import interface as hooks_interface from capellacollab.sessions.hooks import jupyter as jupyter_hook +from capellacollab.sessions.operators import models as operators_models from capellacollab.tools import models as tools_models -from capellacollab.users import models as users_models @pytest.mark.usefixtures("project_user") def test_jupyter_successful_volume_mount( jupyter_model: toolmodels_models.DatabaseToolModel, jupyter_tool: tools_models.DatabaseTool, - user: users_models.DatabaseUser, - db: orm.Session, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): + class MockOperator: # pylint: disable=unused-argument def persistent_volume_exists(self, name: str) -> bool: return True + configuration_hook_request.operator = MockOperator() # type: ignore + configuration_hook_request.tool = jupyter_tool + result = jupyter_hook.JupyterIntegration().configuration_hook( - db=db, user=user, tool=jupyter_tool, operator=MockOperator() + configuration_hook_request ) assert not result["warnings"] assert len(result["volumes"]) == 1 + + volume = result["volumes"][0] + assert isinstance(volume, operators_models.PersistentVolume) assert ( - result["volumes"][0].volume_name + volume.volume_name == "shared-workspace-" + jupyter_model.configuration["workspace"] ) @@ -38,16 +44,18 @@ def persistent_volume_exists(self, name: str) -> bool: @pytest.mark.usefixtures("project_user", "jupyter_model") def test_jupyter_volume_mount_not_found( jupyter_tool: tools_models.DatabaseTool, - user: users_models.DatabaseUser, - db: orm.Session, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): class MockOperator: # pylint: disable=unused-argument def persistent_volume_exists(self, name: str) -> bool: return False + configuration_hook_request.operator = MockOperator() # type: ignore + configuration_hook_request.tool = jupyter_tool + result = jupyter_hook.JupyterIntegration().configuration_hook( - db=db, user=user, tool=jupyter_tool, operator=MockOperator() + configuration_hook_request ) assert not result["volumes"] @@ -60,14 +68,12 @@ def persistent_volume_exists(self, name: str) -> bool: @pytest.mark.usefixtures("jupyter_model") def test_jupyter_volume_mount_without_project_access( jupyter_tool: tools_models.DatabaseTool, - user: users_models.DatabaseUser, - db: orm.Session, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): - class MockOperator: - pass + configuration_hook_request.tool = jupyter_tool result = jupyter_hook.JupyterIntegration().configuration_hook( - db=db, user=user, tool=jupyter_tool, operator=MockOperator() + configuration_hook_request ) assert not result["volumes"] diff --git a/backend/tests/sessions/hooks/test_networking_hook.py b/backend/tests/sessions/hooks/test_networking_hook.py index 93934c255c..8219e7ad04 100644 --- a/backend/tests/sessions/hooks/test_networking_hook.py +++ b/backend/tests/sessions/hooks/test_networking_hook.py @@ -5,14 +5,13 @@ import kubernetes.client import pytest -from capellacollab.sessions import models as sessions_models -from capellacollab.sessions import operators +from capellacollab.sessions.hooks import interface as session_hooks_interface from capellacollab.sessions.hooks import networking as networking_hook -from capellacollab.users import models as users_models def test_network_policy_created( - user: users_models.DatabaseUser, monkeypatch: pytest.MonkeyPatch + monkeypatch: pytest.MonkeyPatch, + post_session_creation_hook_request: session_hooks_interface.PostSessionCreationHookRequest, ): network_policy_counter = 0 @@ -32,16 +31,15 @@ def mock_create_namespaced_network_policy( ) networking_hook.NetworkingIntegration().post_session_creation_hook( - session_id="test", - operator=operators.KubernetesOperator(), - user=user, + post_session_creation_hook_request ) assert network_policy_counter == 1 def test_network_policy_deleted( - session: sessions_models.DatabaseSession, monkeypatch: pytest.MonkeyPatch + monkeypatch: pytest.MonkeyPatch, + pre_session_termination_hook_request: session_hooks_interface.PreSessionTerminationHookRequest, ): network_policy_del_counter = 0 @@ -61,8 +59,7 @@ def mock_delete_namespaced_network_policy( ) networking_hook.NetworkingIntegration().pre_session_termination_hook( - operator=operators.KubernetesOperator(), - session=session, + pre_session_termination_hook_request ) assert network_policy_del_counter == 1 diff --git a/backend/tests/sessions/hooks/test_persistent_workspace.py b/backend/tests/sessions/hooks/test_persistent_workspace.py index 56c6ca303f..597039ce27 100644 --- a/backend/tests/sessions/hooks/test_persistent_workspace.py +++ b/backend/tests/sessions/hooks/test_persistent_workspace.py @@ -11,41 +11,34 @@ from capellacollab.sessions import operators from capellacollab.sessions.hooks import interface as hooks_interface from capellacollab.sessions.hooks import persistent_workspace -from capellacollab.tools import models as tools_models from capellacollab.users import models as users_models from capellacollab.users.workspaces import crud as users_workspaces_crud from capellacollab.users.workspaces import models as users_workspaces_models def test_persistent_workspace_mounting_not_allowed( - db: orm.Session, - tool: tools_models.DatabaseTool, - test_user: users_models.DatabaseUser, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): - tool.config.persistent_workspaces.mounting_enabled = False + configuration_hook_request.tool.config.persistent_workspaces.mounting_enabled = ( + False + ) with pytest.raises(sessions_exceptions.WorkspaceMountingNotAllowedError): persistent_workspace.PersistentWorkspaceHook().configuration_hook( - db=db, - operator=operators.KubernetesOperator(), - user=test_user, - session_type=sessions_models.SessionType.PERSISTENT, - tool=tool, + configuration_hook_request ) def persistent_workspace_mounting_readonly_session( - db: orm.Session, - tool: tools_models.DatabaseTool, - test_user: users_models.DatabaseUser, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): + configuration_hook_request.session_type = ( + sessions_models.SessionType.READONLY + ) + response = ( persistent_workspace.PersistentWorkspaceHook().configuration_hook( - db=db, - operator=operators.KubernetesOperator(), - user=test_user, - session_type=sessions_models.SessionType.READONLY, - tool=tool, + configuration_hook_request ) ) @@ -54,9 +47,9 @@ def persistent_workspace_mounting_readonly_session( def test_workspace_is_created( db: orm.Session, - tool: tools_models.DatabaseTool, test_user: users_models.DatabaseUser, monkeypatch: pytest.MonkeyPatch, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): created_volumes = 0 volume_name = None @@ -80,12 +73,11 @@ def mock_create_namespaced_persistent_volume_claim( assert ( len(users_workspaces_crud.get_workspaces_for_user(db, test_user)) == 0 ) + + configuration_hook_request.operator = operators.KubernetesOperator() + configuration_hook_request.user = test_user persistent_workspace.PersistentWorkspaceHook().configuration_hook( - db=db, - operator=operators.KubernetesOperator(), - user=test_user, - session_type=sessions_models.SessionType.PERSISTENT, - tool=tool, + configuration_hook_request ) assert created_volumes == 1 assert isinstance(volume_name, str) @@ -97,10 +89,10 @@ def mock_create_namespaced_persistent_volume_claim( def test_existing_workspace_is_mounted( db: orm.Session, - tool: tools_models.DatabaseTool, test_user: users_models.DatabaseUser, user_workspace: users_workspaces_models.DatabaseWorkspace, monkeypatch: pytest.MonkeyPatch, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): created_volumes = 0 volume_name = None @@ -120,12 +112,11 @@ def mock_create_namespaced_persistent_volume_claim(self, ns, pvc): assert ( len(users_workspaces_crud.get_workspaces_for_user(db, test_user)) == 1 ) + + configuration_hook_request.user = test_user + configuration_hook_request.operator = operators.KubernetesOperator() persistent_workspace.PersistentWorkspaceHook().configuration_hook( - db=db, - operator=operators.KubernetesOperator(), - user=test_user, - session_type=sessions_models.SessionType.PERSISTENT, - tool=tool, + configuration_hook_request ) assert created_volumes == 1 assert isinstance(volume_name, str) diff --git a/backend/tests/sessions/hooks/test_pre_authentiation_hook.py b/backend/tests/sessions/hooks/test_pre_authentiation_hook.py index 50ef300b58..72b08f82e6 100644 --- a/backend/tests/sessions/hooks/test_pre_authentiation_hook.py +++ b/backend/tests/sessions/hooks/test_pre_authentiation_hook.py @@ -1,29 +1,22 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import logging - import pytest from capellacollab.sessions import auth as sessions_auth -from capellacollab.sessions import models as sessions_models from capellacollab.sessions.hooks import authentication -from capellacollab.users import models as users_models +from capellacollab.sessions.hooks import interface as sessions_hooks_interface def test_pre_authentication_hook( - session: sessions_models.DatabaseSession, - user: users_models.DatabaseUser, - logger: logging.LoggerAdapter, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, monkeypatch: pytest.MonkeyPatch, ): private_key = sessions_auth.generate_private_key() monkeypatch.setattr(sessions_auth, "PRIVATE_KEY", private_key) result = authentication.PreAuthenticationHook().session_connection_hook( - db_session=session, - user=user, - logger=logger, + session_connection_hook_request ) assert "ccm_session_token" in result["cookies"] diff --git a/backend/tests/sessions/hooks/test_project_scope.py b/backend/tests/sessions/hooks/test_project_scope.py new file mode 100644 index 0000000000..6a9143771d --- /dev/null +++ b/backend/tests/sessions/hooks/test_project_scope.py @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from capellacollab.projects import models as projects_models +from capellacollab.sessions.hooks import interface as hooks_interface +from capellacollab.sessions.hooks import project_scope as project_scope_hook + + +def test_correct_workspace_with_project_scope( + configuration_hook_request: hooks_interface.ConfigurationHookRequest, + project: projects_models.DatabaseProject, +): + """Test that the correct workspace is set with the project scope""" + + configuration_hook_request.project_scope = project + result = project_scope_hook.ProjectScopeHook().configuration_hook( + configuration_hook_request + ) + + assert ( + result["environment"]["WORKSPACE_DIR"] + == f"/workspace/{project.slug}/tool-1" + ) diff --git a/backend/tests/sessions/hooks/test_provisioning_hook.py b/backend/tests/sessions/hooks/test_provisioning_hook.py index fe83c0c2cd..0c6ac69534 100644 --- a/backend/tests/sessions/hooks/test_provisioning_hook.py +++ b/backend/tests/sessions/hooks/test_provisioning_hook.py @@ -5,44 +5,50 @@ import pytest from sqlalchemy import orm +from capellacollab.projects import crud as projects_crud from capellacollab.projects import models as projects_models from capellacollab.projects.toolmodels import models as toolmodels_models from capellacollab.projects.toolmodels.modelsources.git import ( models as git_models, ) +from capellacollab.projects.toolmodels.provisioning import ( + crud as provisioning_crud, +) +from capellacollab.projects.toolmodels.provisioning import ( + models as provisioning_models, +) from capellacollab.sessions import exceptions as sessions_exceptions from capellacollab.sessions import models as sessions_models +from capellacollab.sessions.hooks import interface as hooks_interface from capellacollab.sessions.hooks import provisioning as hooks_provisioning from capellacollab.tools import models as tools_models from capellacollab.users import models as users_models +@pytest.mark.asyncio @pytest.mark.usefixtures("project_user") -def test_git_models_are_resolved_correctly( - db: orm.Session, - user: users_models.DatabaseUser, - capella_tool: tools_models.DatabaseTool, - capella_tool_version: tools_models.DatabaseVersion, +async def test_read_only_git_models_are_resolved_correctly( project: projects_models.DatabaseProject, capella_model: toolmodels_models.DatabaseToolModel, git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): - """Make sure that the Git models are correctly translated to GIT_MODELS environment""" - - response = hooks_provisioning.ProvisionWorkspaceHook().configuration_hook( - db=db, - tool=capella_tool, - tool_version=capella_tool_version, - user=user, - provisioning=[ - sessions_models.SessionProvisioningRequest( - project_slug=project.slug, - model_slug=capella_model.slug, - git_model_id=git_model.id, - revision="test", - deep_clone=False, - ) - ], + """Make sure that the Git models are correctly translated to environment""" + configuration_hook_request.session_type = ( + sessions_models.SessionType.READONLY + ) + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="test", + deep_clone=False, + ) + ] + + response = await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request ) expected_response_dict = { @@ -67,43 +73,37 @@ def test_git_models_are_resolved_correctly( ] -def test_provisioning_fails_missing_permission( - db: orm.Session, - user: users_models.DatabaseUser, - capella_tool: tools_models.DatabaseTool, - capella_tool_version: tools_models.DatabaseVersion, +@pytest.mark.asyncio +async def test_provisioning_fails_missing_permission( project: projects_models.DatabaseProject, capella_model: toolmodels_models.DatabaseToolModel, git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """Make sure that provisioning fails when the user does not have the correct permissions""" + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] with pytest.raises(fastapi.HTTPException): - hooks_provisioning.ProvisionWorkspaceHook().configuration_hook( - db=db, - tool=capella_tool, - tool_version=capella_tool_version, - user=user, - provisioning=[ - sessions_models.SessionProvisioningRequest( - project_slug=project.slug, - model_slug=capella_model.slug, - git_model_id=git_model.id, - revision="main", - deep_clone=False, - ) - ], + await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request ) +@pytest.mark.asyncio @pytest.mark.usefixtures("project_user") -def test_provisioning_fails_too_many_models_requested( - db: orm.Session, - user: users_models.DatabaseUser, +async def test_provisioning_fails_too_many_models_requested( capella_tool: tools_models.DatabaseTool, - capella_tool_version: tools_models.DatabaseVersion, project: projects_models.DatabaseProject, capella_model: toolmodels_models.DatabaseToolModel, git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): capella_tool.config.provisioning.max_number_of_models = 1 @@ -115,58 +115,52 @@ def test_provisioning_fails_too_many_models_requested( deep_clone=False, ) + configuration_hook_request.provisioning = [ + session_provisioning_request, + session_provisioning_request, + ] with pytest.raises( sessions_exceptions.TooManyModelsRequestedToProvisionError ): - hooks_provisioning.ProvisionWorkspaceHook().configuration_hook( - db=db, - tool=capella_tool, - tool_version=capella_tool_version, - user=user, - provisioning=[ - session_provisioning_request, - session_provisioning_request, - ], + await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request ) -def test_tool_model_mismatch( - db: orm.Session, - user: users_models.DatabaseUser, - tool: tools_models.DatabaseTool, - tool_version: tools_models.DatabaseVersion, +@pytest.mark.asyncio +async def test_tool_model_mismatch( project: projects_models.DatabaseProject, capella_model: toolmodels_models.DatabaseToolModel, + tool_version: tools_models.DatabaseVersion, git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """Make sure that provisioning fails when the provided model doesn't match the selected tool""" + configuration_hook_request.tool_version = tool_version + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] with pytest.raises(sessions_exceptions.ToolAndModelMismatchError): - hooks_provisioning.ProvisionWorkspaceHook().configuration_hook( - db=db, - tool=tool, - tool_version=tool_version, - user=user, - provisioning=[ - sessions_models.SessionProvisioningRequest( - project_slug=project.slug, - model_slug=capella_model.slug, - git_model_id=git_model.id, - revision="main", - deep_clone=False, - ) - ], + await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request ) -def test_provision_session_with_compatible_tool_versions( +@pytest.mark.asyncio +async def test_read_only_provisioning_session_with_compatible_tool_versions( db: orm.Session, - admin: users_models.DatabaseUser, tool_version: tools_models.DatabaseVersion, - tool: tools_models.DatabaseTool, capella_tool_version: tools_models.DatabaseVersion, project: projects_models.DatabaseProject, capella_model: toolmodels_models.DatabaseToolModel, git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """Make sure that provisioning is successful when the tool is compatible with the tool of the model""" @@ -174,19 +168,222 @@ def test_provision_session_with_compatible_tool_versions( orm.attributes.flag_modified(tool_version, "config") db.commit() - response = hooks_provisioning.ProvisionWorkspaceHook().configuration_hook( - db=db, - tool=tool, - tool_version=tool_version, - user=admin, - provisioning=[ - sessions_models.SessionProvisioningRequest( - project_slug=project.slug, - model_slug=capella_model.slug, - git_model_id=git_model.id, - revision="main", - deep_clone=False, - ) - ], + configuration_hook_request.session_type = ( + sessions_models.SessionType.READONLY + ) + + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] + configuration_hook_request.user.role = users_models.Role.ADMIN + response = await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request ) assert response["environment"]["CAPELLACOLLAB_SESSION_PROVISIONING"] + + +@pytest.mark.asyncio +async def test_request_fails_if_provisioning_is_required( + configuration_hook_request: hooks_interface.ConfigurationHookRequest, +): + """Test that a request without provisioning information fails + + If the tool requires provisioning, but no provisioning information + is provided, the request should fail. + """ + + configuration_hook_request.tool.config.provisioning.required = True + + with pytest.raises(sessions_exceptions.ProvisioningRequiredError): + await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request + ) + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ls_remote", "project_user") +async def test_persistent_provisioning_init( + db: orm.Session, + project: projects_models.DatabaseProject, + user: users_models.DatabaseUser, + capella_model: toolmodels_models.DatabaseToolModel, + git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, +): + """Test the initial provisioning of a persistent provisioning""" + + configuration_hook_request.project_scope = project + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] + response = await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request + ) + + provisioning = provisioning_crud.get_model_provisioning( + db, capella_model, user + ) + assert provisioning is not None + assert ( + provisioning.commit_hash == "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847" + ) + + init_provisioning = response["init_environment"][ + "CAPELLACOLLAB_PROVISIONING" + ] + assert len(init_provisioning) == 1 + assert ( + init_provisioning[0]["revision"] + == "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847" + ) + + session_provisioning = response["environment"][ + "CAPELLACOLLAB_SESSION_PROVISIONING" + ] + assert len(session_provisioning) == 1 + assert "password" not in session_provisioning[0] + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("project_user") +async def test_persistent_provisioning( + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, + provisioning: provisioning_models.DatabaseModelProvisioning, +): + """Test skipping the provisioning if already provisioned""" + + configuration_hook_request.project_scope = project + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] + response = await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request + ) + + assert len(response["init_environment"]["CAPELLACOLLAB_PROVISIONING"]) == 0 + + session_provisioning = response["environment"][ + "CAPELLACOLLAB_SESSION_PROVISIONING" + ] + assert len(session_provisioning) == 1 + assert "password" not in session_provisioning[0] + assert session_provisioning[0]["revision"] == provisioning.commit_hash + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("project_user") +async def test_persistent_provisioning_required_project_scope( + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, +): + """Test that a request without project_scope is declined""" + + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] + + with pytest.raises(sessions_exceptions.ProjectScopeRequiredError): + await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request + ) + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("project_user") +async def test_persistent_provisioning_project_mismatch( + db: orm.Session, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, +): + """If a provisioning is requested for a another project, fail.""" + + project2 = projects_crud.create_project(db, "project2") + configuration_hook_request.project_scope = project2 + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + revision="main", + deep_clone=False, + ) + ] + + with pytest.raises(sessions_exceptions.ProjectAndModelMismatchError): + await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request + ) + + +@pytest.mark.asyncio +@pytest.mark.usefixtures("mock_ls_remote", "project_user") +async def test_provisioning_fallback_without_revision( + db: orm.Session, + project: projects_models.DatabaseProject, + user: users_models.DatabaseUser, + capella_model: toolmodels_models.DatabaseToolModel, + git_model: git_models.DatabaseGitModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, +): + """Test that the provisioning falls back to the default revision + if no provision is provided""" + + configuration_hook_request.project_scope = project + configuration_hook_request.provisioning = [ + sessions_models.SessionProvisioningRequest( + project_slug=project.slug, + model_slug=capella_model.slug, + git_model_id=git_model.id, + deep_clone=False, + ) + ] + + response = await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request + ) + + provisioning = provisioning_crud.get_model_provisioning( + db, capella_model, user + ) + assert provisioning is not None + assert provisioning.revision == git_model.revision + + session_provisioning = response["environment"][ + "CAPELLACOLLAB_SESSION_PROVISIONING" + ] + assert len(session_provisioning) == 1 + assert ( + session_provisioning[0]["revision"] + == "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847" + ) diff --git a/backend/tests/sessions/hooks/test_pure_variants.py b/backend/tests/sessions/hooks/test_pure_variants.py index 6f23039e6e..a16f3e79fe 100644 --- a/backend/tests/sessions/hooks/test_pure_variants.py +++ b/backend/tests/sessions/hooks/test_pure_variants.py @@ -18,7 +18,6 @@ from capellacollab.sessions.hooks import pure_variants from capellacollab.tools import crud as tools_crud from capellacollab.tools import models as tools_models -from capellacollab.users import models as users_models @pytest.fixture(name="pure_variants_tool") @@ -51,16 +50,17 @@ def fixture_pure_variants_model( def test_skip_for_read_only_sessions( - db: orm.Session, - user: users_models.DatabaseUser, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """pure::variants has no read-only support Therefore, the hook also shouldn't do anything for read-only sessions. """ - + configuration_hook_request.session_type = ( + sessions_models.SessionType.READONLY + ) result = pure_variants.PureVariantsIntegration().configuration_hook( - db, user, sessions_models.SessionType.READONLY + configuration_hook_request ) assert result == hooks_interface.ConfigurationHookResult() @@ -69,8 +69,8 @@ def test_skip_for_read_only_sessions( @pytest.mark.usefixtures("project_user") def test_skip_when_user_has_no_pv_access( db: orm.Session, - user: users_models.DatabaseUser, pure_variants_model: toolmodels_models.DatabaseToolModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """If a user has no access to a project with a model that has the pure::variants restriction enabled, skip loading of the license. @@ -84,7 +84,7 @@ def test_skip_when_user_has_no_pv_access( ) result = pure_variants.PureVariantsIntegration().configuration_hook( - db, user, sessions_models.SessionType.PERSISTENT + configuration_hook_request ) assert "environment" not in result @@ -95,8 +95,8 @@ def test_skip_when_user_has_no_pv_access( @pytest.mark.usefixtures("project_user") def test_skip_when_license_server_not_configured( db: orm.Session, - user: users_models.DatabaseUser, pure_variants_model: toolmodels_models.DatabaseToolModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """If no pure::variants license is configured in the settings, skip loading of the license. @@ -111,7 +111,7 @@ def test_skip_when_license_server_not_configured( ) result = pure_variants.PureVariantsIntegration().configuration_hook( - db, user, sessions_models.SessionType.PERSISTENT + configuration_hook_request ) assert "environment" not in result @@ -122,8 +122,8 @@ def test_skip_when_license_server_not_configured( @pytest.mark.usefixtures("project_user", "pure_variants_license") def test_inject_pure_variants_license_information( db: orm.Session, - user: users_models.DatabaseUser, pure_variants_model: toolmodels_models.DatabaseToolModel, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """Test that the configured license information is properly injected in the session container. @@ -138,7 +138,7 @@ def test_inject_pure_variants_license_information( ) result = pure_variants.PureVariantsIntegration().configuration_hook( - db, user, sessions_models.SessionType.PERSISTENT + configuration_hook_request ) assert result["environment"] == { diff --git a/backend/tests/sessions/hooks/test_session_preparation.py b/backend/tests/sessions/hooks/test_session_preparation.py index ab12c919a4..2677774b34 100644 --- a/backend/tests/sessions/hooks/test_session_preparation.py +++ b/backend/tests/sessions/hooks/test_session_preparation.py @@ -5,33 +5,32 @@ from capellacollab.sessions import models as sessions_models from capellacollab.sessions.hooks import interface as hooks_interface from capellacollab.sessions.hooks import session_preparation -from capellacollab.tools import models as tools_models -def test_session_preparation_hook(tool: tools_models.DatabaseTool): +def test_session_preparation_hook( + configuration_hook_request: hooks_interface.ConfigurationHookRequest, +): """Test that the session preparation hook registers a shared volume""" - + configuration_hook_request.session_type = ( + sessions_models.SessionType.READONLY + ) result = session_preparation.GitRepositoryCloningHook().configuration_hook( - session_type=sessions_models.SessionType.READONLY, - session_id="session-id", - tool=tool, + configuration_hook_request ) assert len(result["volumes"]) == 1 assert len(result["init_volumes"]) == 1 assert result["volumes"][0] == result["init_volumes"][0] - assert result["volumes"][0].name == "session-id-models" + assert result["volumes"][0].name == "nxylxqbmfqwvswlqlcbsirvrt-models" def test_session_preparation_hook_with_persistent_session( - tool: tools_models.DatabaseTool, + configuration_hook_request: hooks_interface.ConfigurationHookRequest, ): """Test that the session preparation hook doesn't do anything for persistent sessions""" result = session_preparation.GitRepositoryCloningHook().configuration_hook( - session_type=sessions_models.SessionType.PERSISTENT, - session_id="session-id", - tool=tool, + configuration_hook_request ) assert result == hooks_interface.ConfigurationHookResult() diff --git a/backend/tests/sessions/hooks/test_t4c_hook.py b/backend/tests/sessions/hooks/test_t4c_hook.py index e058e96ae2..3a26641d1d 100644 --- a/backend/tests/sessions/hooks/test_t4c_hook.py +++ b/backend/tests/sessions/hooks/test_t4c_hook.py @@ -53,16 +53,12 @@ def fixture_mock_add_user_to_repository_failed( @responses.activate @pytest.mark.usefixtures("t4c_model", "project_user") def test_t4c_configuration_hook( - db: orm.Session, user: users_models.DatabaseUser, - capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository: responses.BaseResponse, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): result = t4c.T4CIntegration().configuration_hook( - db=db, - user=user, - tool_version=capella_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert result["environment"]["T4C_LICENCE_SECRET"] @@ -76,21 +72,20 @@ def test_t4c_configuration_hook( @responses.activate @pytest.mark.usefixtures("t4c_model") def test_t4c_configuration_hook_as_admin( - db: orm.Session, - admin: users_models.DatabaseUser, - capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository: responses.BaseResponse, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): + configuration_hook_request.user.role = users_models.Role.ADMIN result = t4c.T4CIntegration().configuration_hook( - db=db, - user=admin, - tool_version=capella_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert result["environment"]["T4C_LICENCE_SECRET"] assert len(json.loads(result["environment"]["T4C_JSON"])) == 1 - assert result["environment"]["T4C_USERNAME"] == admin.name + assert ( + result["environment"]["T4C_USERNAME"] + == configuration_hook_request.user.name + ) assert result["environment"]["T4C_PASSWORD"] assert not result["warnings"] assert mock_add_user_to_repository.call_count == 1 @@ -100,11 +95,11 @@ def test_t4c_configuration_hook_as_admin( @pytest.mark.usefixtures("t4c_model") def test_t4c_configuration_hook_with_same_repository_used_twice( db: orm.Session, - admin: users_models.DatabaseUser, project: projects_models.DatabaseProject, capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository: responses.BaseResponse, t4c_repository: settings_t4c_repositories_models.DatabaseT4CRepository, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): model = toolmodels_models.PostToolModel( name="test2", description="test", tool_id=capella_tool_version.tool.id @@ -113,11 +108,9 @@ def test_t4c_configuration_hook_with_same_repository_used_twice( db, project, model, capella_tool_version.tool, capella_tool_version ) models_t4c_crud.create_t4c_model(db, db_model, t4c_repository, "default2") + configuration_hook_request.user.role = users_models.Role.ADMIN result = t4c.T4CIntegration().configuration_hook( - db=db, - user=admin, - tool_version=capella_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert len(json.loads(result["environment"]["T4C_JSON"])) == 1 @@ -128,16 +121,14 @@ def test_t4c_configuration_hook_with_same_repository_used_twice( @responses.activate @pytest.mark.usefixtures("t4c_model", "project_user") def test_t4c_configuration_hook_failure( - db: orm.Session, user: users_models.DatabaseUser, - capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository_failed: responses.BaseResponse, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): + """Test behavior when T4C API call fails""" + result = t4c.T4CIntegration().configuration_hook( - db=db, - user=user, - tool_version=capella_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert result["environment"]["T4C_LICENCE_SECRET"] @@ -154,17 +145,14 @@ def test_configuration_hook_for_archived_project( project: projects_models.DatabaseProject, db: orm.Session, user: users_models.DatabaseUser, - capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository: responses.BaseResponse, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): project.is_archived = True db.commit() result = t4c.T4CIntegration().configuration_hook( - db=db, - user=user, - tool_version=capella_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert not result["environment"]["T4C_LICENCE_SECRET"] @@ -180,18 +168,15 @@ def test_configuration_hook_for_archived_project( def test_configuration_hook_as_rw_user( db: orm.Session, user: users_models.DatabaseUser, - capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository: responses.BaseResponse, project_user: projects_users_models.ProjectUserAssociation, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): project_user.permission = projects_users_models.ProjectUserPermission.READ db.commit() result = t4c.T4CIntegration().configuration_hook( - db=db, - user=user, - tool_version=capella_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert not result["environment"]["T4C_LICENCE_SECRET"] @@ -209,6 +194,7 @@ def test_configuration_hook_for_compatible_tool( user: users_models.DatabaseUser, capella_tool_version: tools_models.DatabaseVersion, mock_add_user_to_repository: responses.BaseResponse, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): custom_tool = tools_crud.create_tool( db, tools_models.CreateTool(name="custom") @@ -223,11 +209,9 @@ def test_configuration_hook_for_compatible_tool( db, custom_tool, create_compatible_tool_version ) + configuration_hook_request.tool_version = compatible_tool_version result = t4c.T4CIntegration().configuration_hook( - db=db, - user=user, - tool_version=compatible_tool_version, - session_type=sessions_models.SessionType.PERSISTENT, + configuration_hook_request ) assert result["environment"]["T4C_LICENCE_SECRET"] @@ -239,28 +223,25 @@ def test_configuration_hook_for_compatible_tool( def test_t4c_configuration_hook_non_persistent( - db: orm.Session, - user: users_models.DatabaseUser, - tool_version: tools_models.DatabaseVersion, + configuration_hook_request: sessions_hooks_interface.ConfigurationHookRequest, ): + configuration_hook_request.session_type = ( + sessions_models.SessionType.READONLY + ) result = t4c.T4CIntegration().configuration_hook( - db=db, - user=user, - tool_version=tool_version, - session_type=sessions_models.SessionType.READONLY, + configuration_hook_request ) assert result == sessions_hooks_interface.ConfigurationHookResult() def test_t4c_connection_hook_non_persistent( - user: users_models.DatabaseUser, session: sessions_models.DatabaseSession, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, ): session.type = sessions_models.SessionType.READONLY result = t4c.T4CIntegration().session_connection_hook( - db_session=session, - user=user, + session_connection_hook_request ) assert result == sessions_hooks_interface.SessionConnectionHookResult() @@ -268,8 +249,8 @@ def test_t4c_connection_hook_non_persistent( def test_t4c_connection_hook_shared_session( db: orm.Session, - user: users_models.DatabaseUser, session: sessions_models.DatabaseSession, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, ): user2 = users_crud.create_user( db, @@ -280,21 +261,19 @@ def test_t4c_connection_hook_shared_session( ) session.owner = user2 result = t4c.T4CIntegration().session_connection_hook( - db_session=session, - user=user, + session_connection_hook_request ) assert result == sessions_hooks_interface.SessionConnectionHookResult() def test_t4c_connection_hook( - user: users_models.DatabaseUser, session: sessions_models.DatabaseSession, + session_connection_hook_request: sessions_hooks_interface.SessionConnectionHookRequest, ): session.environment = {"T4C_PASSWORD": "test"} result = t4c.T4CIntegration().session_connection_hook( - db_session=session, - user=user, + session_connection_hook_request ) assert result["t4c_token"] == "test" @@ -303,18 +282,21 @@ def test_t4c_connection_hook( @responses.activate @pytest.mark.usefixtures("t4c_model", "project_user") def test_t4c_termination_hook( - db: orm.Session, session: sessions_models.DatabaseSession, user: users_models.DatabaseUser, t4c_instance: t4c_models.DatabaseT4CInstance, capella_tool_version: tools_models.DatabaseVersion, + pre_session_termination_hook_request: sessions_hooks_interface.PreSessionTerminationHookRequest, ): session.version = capella_tool_version + pre_session_termination_hook_request.session = session rsp = responses.delete( f"{t4c_instance.rest_api}/users/{user.name}?repositoryName=test", status=200, ) - t4c.T4CIntegration().pre_session_termination_hook(db=db, session=session) + t4c.T4CIntegration().pre_session_termination_hook( + pre_session_termination_hook_request + ) assert rsp.call_count == 1 diff --git a/backend/tests/sessions/test_session_environment.py b/backend/tests/sessions/test_session_environment.py index 8871192b5c..763ccb4c99 100644 --- a/backend/tests/sessions/test_session_environment.py +++ b/backend/tests/sessions/test_session_environment.py @@ -4,9 +4,11 @@ import logging import pytest +from sqlalchemy import orm from capellacollab import config from capellacollab.config import models as config_models +from capellacollab.core import models as core_models from capellacollab.sessions import crud as sessions_crud from capellacollab.sessions import hooks as sessions_hooks from capellacollab.sessions import models as sessions_models @@ -18,7 +20,7 @@ class MockOperator: - environment = {} + environment: dict[str, str] = {} # pylint: disable=unused-argument def start_session(self, environment, *args, **kwargs): @@ -103,15 +105,17 @@ def fixture_patch_irrelevant_request_session_calls( ) +@pytest.mark.asyncio @pytest.mark.usefixtures( "patch_irrelevant_request_session_calls", "tool_version" ) -def test_environment_behaviour( +async def test_environment_behavior( monkeypatch: pytest.MonkeyPatch, operator: MockOperator, logger: logging.LoggerAdapter, + db: orm.Session, ): - """Test the behaviour of environment variables + """Test the behavior of environment variables The rules are: @@ -123,7 +127,7 @@ def test_environment_behaviour( """ class GetSessionsReponseMock: - warnings = [] + warnings: list[core_models.Message] = [] response = GetSessionsReponseMock() @@ -133,7 +137,7 @@ class GetSessionsReponseMock: lambda *args: response, ) - sessions_routes.request_session( + await sessions_routes.request_session( sessions_models.PostSessionRequest( tool_id=0, version_id=0, @@ -144,8 +148,8 @@ class GetSessionsReponseMock: users_models.DatabaseUser( name="test", idp_identifier="test", role=users_models.Role.USER ), - None, - operator, + db, + operator, # type: ignore logger, ) @@ -175,7 +179,6 @@ class GetSessionsReponseMock: def test_environment_resolution_before_stage(logger: logging.LoggerAdapter): - environment = {"TEST": [{"test": "test2"}]} rules = { "TEST2": tools_models.ToolSessionEnvironment( diff --git a/backend/tests/sessions/test_session_hooks.py b/backend/tests/sessions/test_session_hooks.py index 376d9ba9bb..0258e99065 100644 --- a/backend/tests/sessions/test_session_hooks.py +++ b/backend/tests/sessions/test_session_hooks.py @@ -39,57 +39,37 @@ def create_persistent_volume(self, *args, **kwargs): class TestSessionHook(hooks_interface.HookRegistration): configuration_hook_counter = 0 + async_configuration_hook_counter = 0 post_session_creation_hook_counter = 0 session_connection_hook_counter = 0 post_termination_hook_counter = 0 def configuration_hook( - self, - db: orm.Session, - operator: operators.KubernetesOperator, - user: users_models.DatabaseUser, - tool: tools_models.DatabaseTool, - tool_version: tools_models.DatabaseVersion, - session_type: sessions_models.SessionType, - connection_method: tools_models.ToolSessionConnectionMethod, - provisioning: list[sessions_models.SessionProvisioningRequest], - session_id: str, - **kwargs, + self, request: hooks_interface.ConfigurationHookRequest ) -> hooks_interface.ConfigurationHookResult: self.configuration_hook_counter += 1 return hooks_interface.ConfigurationHookResult() + async def async_configuration_hook( + self, request: hooks_interface.ConfigurationHookRequest + ) -> hooks_interface.ConfigurationHookResult: + self.async_configuration_hook_counter += 1 + return hooks_interface.ConfigurationHookResult() + def post_session_creation_hook( - self, - session_id: str, - session: k8s.Session, - db_session: sessions_models.DatabaseSession, - operator: operators.KubernetesOperator, - user: users_models.DatabaseUser, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + self, request: hooks_interface.PostSessionCreationHookRequest ) -> hooks_interface.PostSessionCreationHookResult: self.post_session_creation_hook_counter += 1 return hooks_interface.PostSessionCreationHookResult() def session_connection_hook( - self, - db: orm.Session, - db_session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - logger: logging.LoggerAdapter, - **kwargs, + self, request: hooks_interface.SessionConnectionHookRequest ) -> hooks_interface.SessionConnectionHookResult: self.session_connection_hook_counter += 1 return hooks_interface.SessionConnectionHookResult() def pre_session_termination_hook( - self, - db: orm.Session, - operator: operators.KubernetesOperator, - session: sessions_models.DatabaseSession, - connection_method: tools_models.ToolSessionConnectionMethod, - **kwargs, + self, request: hooks_interface.PreSessionTerminationHookRequest ) -> hooks_interface.PreSessionTerminationHookResult: self.post_termination_hook_counter += 1 return hooks_interface.PreSessionTerminationHookResult() @@ -123,14 +103,16 @@ def get_mock_operator(): del __main__.app.dependency_overrides[operators.get_operator] +@pytest.mark.asyncio @pytest.mark.usefixtures("mock_session_injection", "tool_version") -def test_hook_calls_during_session_request( +async def test_hook_calls_during_session_request( monkeypatch: pytest.MonkeyPatch, db: orm.Session, user: users_models.DatabaseUser, mockoperator: MockOperator, session_hook: TestSessionHook, tool: tools_models.DatabaseTool, + logger: logging.LoggerAdapter, ): """Test that the relevant session hooks are called during a session request. @@ -145,7 +127,7 @@ def test_hook_calls_during_session_request( lambda *args, **kwargs: "placeholder", ) - sessions_routes.request_session( + await sessions_routes.request_session( sessions_models.PostSessionRequest( tool_id=0, version_id=0, @@ -156,10 +138,11 @@ def test_hook_calls_during_session_request( user, db, mockoperator, # type: ignore - logging.getLogger("test"), + logger, ) assert session_hook.configuration_hook_counter == 1 + assert session_hook.async_configuration_hook_counter == 1 assert session_hook.post_session_creation_hook_counter == 1 assert session_hook.session_connection_hook_counter == 0 assert session_hook.post_termination_hook_counter == 0 @@ -168,6 +151,7 @@ def test_hook_calls_during_session_request( def test_hook_call_during_session_connection( db: orm.Session, session: sessions_models.DatabaseSession, + logger: logging.LoggerAdapter, ): """Test that the session hook is called when connecting to a session""" @@ -176,7 +160,7 @@ def test_hook_call_during_session_connection( db, session, session.owner, - logging.getLogger("test"), + logger, ) diff --git a/backend/tests/sessions/test_session_routes.py b/backend/tests/sessions/test_session_routes.py index ce3178b204..8d73affb09 100644 --- a/backend/tests/sessions/test_session_routes.py +++ b/backend/tests/sessions/test_session_routes.py @@ -11,6 +11,7 @@ from sqlalchemy import orm from capellacollab.__main__ import app +from capellacollab.projects import models as projects_models from capellacollab.projects.toolmodels import models as toolmodels_models from capellacollab.projects.toolmodels.modelsources.git import ( models as git_models, @@ -246,3 +247,61 @@ def test_own_sessions( # Check that environment and config are not exposed assert "environment" not in response.json()[0] assert "config" not in response.json()[0] + + +@pytest.mark.usefixtures("kubernetes", "user") +def test_request_session_connection_method_fallback( + client: testclient.TestClient, + tool_version: tools_models.DatabaseVersion, + tool: tools_models.DatabaseTool, +): + """Test missing connection_method_id in the request + + If the connection_method_id is missing in the request, + the first applicable connection method of the tool should be used. + """ + + response = client.post( + "/api/v1/sessions", + json={ + "tool_id": tool.id, + "version_id": tool_version.id, + "session_type": "persistent", + }, + ) + + assert response.status_code == 200 + assert "id" in response.json() + assert ( + response.json()["connection_method_id"] + == tool.config.connection.methods[0].id + ) + + +@pytest.mark.usefixtures("user") +def test_project_slug_for_unauthorized_project( + client: testclient.TestClient, + tool_version: tools_models.DatabaseVersion, + tool: tools_models.DatabaseTool, + project: projects_models.DatabaseProject, +): + """Test project_slug without permission in the request + + Test that a request is declined if the user has no access to the project. + """ + + response = client.post( + "/api/v1/sessions", + json={ + "tool_id": tool.id, + "version_id": tool_version.id, + "session_type": "persistent", + "project_slug": project.slug, + }, + ) + + assert response.status_code == 403 + assert ( + response.json()["detail"]["err_code"] + == "REQUIRED_PROJECT_ROLE_NOT_MET" + ) diff --git a/backend/tests/settings/fixtures.py b/backend/tests/settings/fixtures.py new file mode 100644 index 0000000000..feff4c897f --- /dev/null +++ b/backend/tests/settings/fixtures.py @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import asyncio + +import pytest + +from capellacollab.settings.modelsources.git import core as instances_git_core + + +@pytest.fixture(name="mock_ls_remote") +def fixture_mock_ls_remote( + monkeypatch: pytest.MonkeyPatch, +): + ls_remote = ( + "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847 HEAD\n" + "e0f83d8d57ec1552c5fb76c83f7dff7f0ff86631 refs/heads/test-branch1\n" + "76c71f5468f6e444317146c6c9a3e00033974a1c refs/heads/test-branch2\n" + "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847 refs/heads/main\n" + "ea10a5a82f31807d89c1bb7fc61dcd331e49f8fc refs/pull/100/head\n" + "47cda65668eb258c5e84a8ffd43909ba4fac2661 refs/tags/v1.0.0\n" + "bce139e467d3d60bd21a4097c78e86a87e1a5d21 refs/tags/v1.1.0\n" + ) + + # pylint: disable=unused-argument + def mock_ls_remote(*args, **kwargs): + f: asyncio.Future = asyncio.Future() + f.set_result(ls_remote) + return f + + monkeypatch.setattr( + instances_git_core, "_ls_remote_command", mock_ls_remote + ) diff --git a/backend/tests/settings/test_git_instances.py b/backend/tests/settings/test_git_instances.py index f28fd0451b..a9b7a3607a 100644 --- a/backend/tests/settings/test_git_instances.py +++ b/backend/tests/settings/test_git_instances.py @@ -1,13 +1,11 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 -import asyncio import pytest from fastapi import testclient from sqlalchemy import orm -from capellacollab.settings.modelsources.git import core as git_core from capellacollab.settings.modelsources.git import crud as git_crud from capellacollab.settings.modelsources.git import models as git_models @@ -99,28 +97,10 @@ def test_delete_git_instance( assert not git_crud.get_git_instance_by_id(db, git_instance.id) -@pytest.mark.usefixtures("user") +@pytest.mark.usefixtures("user", "mock_ls_remote") def test_fetch_revisions( - monkeypatch: pytest.MonkeyPatch, client: testclient.TestClient, ): - ls_remote = [ - "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847 HEAD", - "e0f83d8d57ec1552c5fb76c83f7dff7f0ff86631 refs/heads/test-branch1", - "76c71f5468f6e444317146c6c9a3e00033974a1c refs/heads/test-branch2", - "0665eb5bf5dc3a7bdcb30b4354c85eddde2bd847 refs/heads/main", - "ea10a5a82f31807d89c1bb7fc61dcd331e49f8fc refs/pull/100/head", - "47cda65668eb258c5e84a8ffd43909ba4fac2661 refs/tags/v1.0.0", - "bce139e467d3d60bd21a4097c78e86a87e1a5d21 refs/tags/v1.1.0", - ] - - # pylint: disable=unused-argument - def mock_ls_remote(*args, **kwargs): - f: asyncio.Future = asyncio.Future() - f.set_result(ls_remote) - return f - - monkeypatch.setattr(git_core, "ls_remote", mock_ls_remote) response = client.post( "/api/v1/settings/modelsources/git/revisions", diff --git a/backend/tests/test_event_creation.py b/backend/tests/test_event_creation.py index c24e4ff234..cf2fc54174 100644 --- a/backend/tests/test_event_creation.py +++ b/backend/tests/test_event_creation.py @@ -1,6 +1,8 @@ # SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors # SPDX-License-Identifier: Apache-2.0 +import uuid + import pytest import sqlalchemy as sa from fastapi import testclient @@ -17,7 +19,12 @@ reason: str = "TestReason" -def test_create_admin_user_by_system(db): +@pytest.fixture(name="unique_username") +def fixture_unique_username() -> str: + return str(uuid.uuid1()) + + +def test_create_admin_user_by_system(db: orm.Session): user = users_crud.get_user_by_name(db, config.config.initial.admin) assert user is not None @@ -36,7 +43,12 @@ def test_create_admin_user_by_system(db): assert event.user_id == user.id -def test_create_user_created_event(client, db, executor_name, unique_username): +def test_create_user_created_event( + client: testclient.TestClient, + db: orm.Session, + executor_name: str, + unique_username: str, +): executor = users_crud.create_user( db, executor_name, executor_name, None, users_models.Role.ADMIN ) @@ -160,45 +172,39 @@ def test_create_assign_user_role_event( ), ], ) +@pytest.mark.usefixtures("admin") def test_create_user_added_to_project_event( client: testclient.TestClient, db: orm.Session, - executor_name: str, - unique_username: str, + admin: users_models.DatabaseUser, project: projects_models.DatabaseProject, permission: projects_users_models.ProjectUserPermission, expected_permission_event_type: events_models.EventType, + user2: users_models.DatabaseUser, ): - executor = users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - response = client.post( f"/api/v1/projects/{project.slug}/users/", json={ "role": projects_users_models.ProjectUserRole.USER.value, "permission": permission.value, - "username": user.name, + "username": user2.name, "reason": reason, }, ) assert response.status_code == 200 - events = get_events_by_user_id(db, user.id) + events = get_events_by_user_id(db, user2.id) assert len(events) == 3 user_added_event = events[0] assert ( user_added_event.event_type == events_models.EventType.ADDED_TO_PROJECT ) - assert user_added_event.executor_id == executor.id + assert user_added_event.executor_id == admin.id assert user_added_event.reason == reason assert user_added_event.project_id == project.id - assert user_added_event.user_id == user.id + assert user_added_event.user_id == user2.id assert ( events[1].event_type @@ -210,71 +216,57 @@ def test_create_user_added_to_project_event( def test_create_user_removed_from_project_event( client: testclient.TestClient, db: orm.Session, - executor_name: str, - unique_username: str, + user2: users_models.DatabaseUser, + admin: users_models.DatabaseUser, project: projects_models.DatabaseProject, ): - executor = users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - projects_users_crud.add_user_to_project( db, project, - user, + user2, projects_users_models.ProjectUserRole.USER, projects_users_models.ProjectUserPermission.READ, ) response = client.request( "DELETE", - f"/api/v1/projects/{project.slug}/users/{user.id}", + f"/api/v1/projects/{project.slug}/users/{user2.id}", content=reason, headers={"Content-Type": "text/plain"}, ) assert response.status_code == 204 - events = get_events_by_user_id(db, user.id) + events = get_events_by_user_id(db, user2.id) assert len(events) == 1 event = events[0] assert event.event_type == events_models.EventType.REMOVED_FROM_PROJECT - assert event.executor_id == executor.id + assert event.executor_id == admin.id assert event.reason == reason assert event.project_id == project.id - assert event.user_id == user.id + assert event.user_id == user2.id def test_create_manager_added_to_project_event( client: testclient.TestClient, db: orm.Session, - executor_name: str, - unique_username: str, + admin: users_models.DatabaseUser, project: projects_models.DatabaseProject, + user2: users_models.DatabaseUser, ): - executor = users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - response = client.post( f"/api/v1/projects/{project.slug}/users/", json={ "role": projects_users_models.ProjectUserRole.MANAGER.value, "permission": projects_users_models.ProjectUserPermission.READ.value, - "username": user.name, + "username": user2.name, "reason": reason, }, ) - events = get_events_by_user_id(db, user.id) + events = get_events_by_user_id(db, user2.id) assert response.status_code == 200 assert len(events) == 2 @@ -287,10 +279,10 @@ def test_create_manager_added_to_project_event( ], ): assert event.event_type == expected_event_type - assert event.executor_id == executor.id + assert event.executor_id == admin.id assert event.reason == reason assert event.project_id == project.id - assert event.user_id == user.id + assert event.user_id == user2.id @pytest.mark.parametrize( @@ -312,29 +304,23 @@ def test_create_user_permission_change_event( client: testclient.TestClient, db: orm.Session, executor_name: str, - unique_username: str, + admin: users_models.DatabaseUser, project: projects_models.DatabaseProject, initial_permission: projects_users_models.ProjectUserPermission, target_permission: projects_users_models.ProjectUserPermission, expected_permission_event_type: events_models.EventType, + user2: users_models.DatabaseUser, ): - executor = users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - projects_users_crud.add_user_to_project( db, project, - user, + user2, projects_users_models.ProjectUserRole.USER, initial_permission, ) response = client.patch( - f"/api/v1/projects/{project.slug}/users/{user.id}", + f"/api/v1/projects/{project.slug}/users/{user2.id}", json={ "permission": target_permission.value, "reason": reason, @@ -343,16 +329,16 @@ def test_create_user_permission_change_event( assert response.status_code == 204 - events = get_events_by_user_id(db, user.id) + events = get_events_by_user_id(db, user2.id) assert len(events) == 1 event = events[0] assert event.event_type == expected_permission_event_type - assert event.executor_id == executor.id + assert event.executor_id == admin.id assert event.reason == reason assert event.project_id == project.id - assert event.user_id == user.id + assert event.user_id == user2.id @pytest.mark.parametrize( @@ -373,30 +359,23 @@ def test_create_user_permission_change_event( def test_create_user_role_change_event( client: testclient.TestClient, db: orm.Session, - executor_name: str, - unique_username: str, + admin: users_models.DatabaseUser, project: projects_models.DatabaseProject, initial_role: projects_users_models.ProjectUserRole, target_role: projects_users_models.ProjectUserRole, expected_role_event_type: events_models.EventType, + user2: users_models.DatabaseUser, ): - executor = users_crud.create_user( - db, executor_name, executor_name, None, users_models.Role.ADMIN - ) - user = users_crud.create_user( - db, unique_username, unique_username, None, users_models.Role.USER - ) - projects_users_crud.add_user_to_project( db, project, - user, + user2, initial_role, projects_users_models.ProjectUserPermission.READ, ) response = client.patch( - f"/api/v1/projects/{project.slug}/users/{user.id}", + f"/api/v1/projects/{project.slug}/users/{user2.id}", json={ "role": target_role.value, "reason": reason, @@ -405,16 +384,16 @@ def test_create_user_role_change_event( assert response.status_code == 204 - events = get_events_by_user_id(db, user.id) + events = get_events_by_user_id(db, user2.id) assert len(events) == 1 event = events[0] assert event.event_type == expected_role_event_type - assert event.executor_id == executor.id + assert event.executor_id == admin.id assert event.reason == reason assert event.project_id == project.id - assert event.user_id == user.id + assert event.user_id == user2.id def get_events_by_username( diff --git a/backend/tests/users/fixtures.py b/backend/tests/users/fixtures.py index 65d9c1639b..4c99f34979 100644 --- a/backend/tests/users/fixtures.py +++ b/backend/tests/users/fixtures.py @@ -31,11 +31,6 @@ async def cookie_passthrough(self, request: fastapi.Request): return name -@pytest.fixture(name="unique_username") -def fixture_unique_username() -> str: - return str(uuid.uuid1()) - - @pytest.fixture(name="basic_user") def fixture_basic_user( db: orm.Session, executor_name: str @@ -59,6 +54,13 @@ def get_mock_own_user(): del app.dependency_overrides[users_injectables.get_own_user] +@pytest.fixture(name="user2") +def fixture_user2(db: orm.Session) -> users_models.DatabaseUser: + return users_crud.create_user( + db, "user2", "user2", None, users_models.Role.USER + ) + + @pytest.fixture(name="admin") def fixture_admin( db: orm.Session, executor_name: str diff --git a/docs/docs/admin/tools/configuration.md b/docs/docs/admin/tools/configuration.md index e60f844c32..964ccbf00c 100644 --- a/docs/docs/admin/tools/configuration.md +++ b/docs/docs/admin/tools/configuration.md @@ -167,6 +167,13 @@ variables can be used by the tool: The tool has to set the `Content-Security-Policy` header to `frame-ancestors self {CAPELLACOLLAB_ORIGIN_HOST}`. Otherwise, the session viewer can't be used with the tool! + + `WORKSPACE_DIR` + `/workspace` + + The directory of the (persistent) workspace the application should work with. + + diff --git a/docs/docs/user/sessions/types/index.md b/docs/docs/user/sessions/types/index.md index 13c864cf4f..6c387f6ae3 100644 --- a/docs/docs/user/sessions/types/index.md +++ b/docs/docs/user/sessions/types/index.md @@ -3,49 +3,48 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -You can choose two different types of workspaces: +# Session Types -### Persistent Capella/Papyrus Sessions +The Capella Collaboration Manager offers different Session Types: -Persistent Sessions allows you to use personal workspace within Capella. Your -personal workspace will be stored and is part of our backup routines. However, -we still advise not to save any important information there. By default, we +## Persistent Sessions + +Persistent Sessions will store your work in the `/workspace` folder. Persistent +Sessions allows you to use personal workspace within Capella. By default, we will request 20GB of storage for your personal workspace. If your project uses the T4C-workflow, we will suggest all visible models in the T4C connection dialog. -???+ tip - - Starting the first time, your personal workspace will be empty. - Please close the `Welcome`-dialog first: - ![Close Welcome dialog](screenshots/close_welcome_dialog.png) - -!!! info +!!! warning - Only work stored in the `/workspace` folder (default workspace folder) will - be persistent. + Only work stored in the `/workspace` folder (and subdirectories) will + be persistent. If you store your work in another folder, it will be lost + when the session is closed. -### Persistent Jupyter Notebooks +### Provisioned Sessions -Jupyter notebooks allow you to programmatically explore (capella) models. -You'll use the same shared workspace as with persistent Capella/Papyrus -sessions. The same restrictions as with Capella sessions apply here. +Provisioned Sessions are a special type of Persistent Sessions. They are +available in projects and can be used to initialize a workspace with content +from Git repositories. After the initial provisioning, changes will be saved. +You can reset the state at any time to the latest state of the Git repository. -!!! info +Provisioned sessions are a good alternative to persistent sessions if you only +have read-only access in a project but want to make changes on the model that +you want to integrate later. - Jupyter notebooks use the same `/workspace` folder as is used with - Capella sessions. +The provisioned workspace will saved in your personal workspace in the folder +`/workspace/{project_slug}/tool-{tool_id}`. -### Readonly Capella/Papyrus Sessions +## Read-Only Sessions -Readonly Sessions allow you to read information from models without consuming a -license. +Read-Only Sessions allow you to read information from models without the risk +of changing the model. The can be useful if you want to review a model or don't +have permissions to write to the model. -!!! warning +!!! info - Read-only sessions work only with linked git models. Please ask your project - lead if your model has read-only support. + Read-only sessions only work for models with linked Git repositories. !!! danger diff --git a/frontend/.prettierrc.js b/frontend/.prettierrc.js index dc72f5ebbb..569a061ad4 100644 --- a/frontend/.prettierrc.js +++ b/frontend/.prettierrc.js @@ -7,6 +7,8 @@ module.exports = { plugins: [ require.resolve("prettier-plugin-tailwindcss"), require.resolve("@trivago/prettier-plugin-sort-imports"), + require.resolve("prettier-plugin-classnames"), + require.resolve("prettier-plugin-merge"), ], importOrder: ["^[./]"], importOrderParserPlugins: ["typescript", "decorators-legacy"], diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 956634bd27..347cfe5116 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -26,6 +26,7 @@ "highlight.js": "^11.10.0", "http-status-codes": "^2.3.0", "monaco-editor": "^0.52.0", + "ngx-markdown": "^18.1.0", "ngx-skeleton-loader": "^9.0.0", "ngx-toastr": "^19.0.0", "npm": "^10.9.0", @@ -68,6 +69,8 @@ "npm-check-updates": "^17.1.9", "postcss": "^8.4.47", "prettier": "^3.3.3", + "prettier-plugin-classnames": "^0.7.4", + "prettier-plugin-merge": "^0.7.1", "prettier-plugin-tailwindcss": "^0.6.8", "storybook": "^8.4.0", "tailwindcss": "^3.4.14", @@ -815,6 +818,30 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-0.4.1.tgz", + "integrity": "sha512-T7yB5QNG29afhWVkVq7XeIMBa5U/vs9mX69YqayXypPRmYzUmzwnYltplHmPtZ4HPCn+sQKeXW8I47wCbuBOjw==", + "license": "MIT", + "optional": true, + "dependencies": { + "package-manager-detector": "^0.2.0", + "tinyexec": "^0.3.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -2777,6 +2804,57 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.0.tgz", + "integrity": "sha512-o+UlMLt49RvtCASlOMW0AkHnabN9wR9rwCCherxO0yG4Npy34GkvrAqdXQvrhNs+jh+gkK8gB8Lf05qL/O7KWg==", + "license": "MIT", + "optional": true + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0", + "optional": true + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0", + "optional": true + }, "node_modules/@compodoc/compodoc": { "version": "1.1.26", "resolved": "https://registry.npmjs.org/@compodoc/compodoc/-/compodoc-1.1.26.tgz", @@ -3207,6 +3285,19 @@ "node": ">=6" } }, + "node_modules/@compodoc/compodoc/node_modules/marked": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.3.tgz", + "integrity": "sha512-ev2uM40p0zQ/GbvqotfKcSWEa59fJwluGZj5dcaUOwDRrB1F3dncdXy8NWUApk4fi8atU3kTBOwjyjZ0ud0dxw==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, "node_modules/@compodoc/compodoc/node_modules/readdirp": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", @@ -3910,9 +4001,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz", - "integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.14.0.tgz", + "integrity": "sha512-pFoEtFWCPyDOl+C6Ift+wC7Ro89otjigCf5vcuWqWgqNSQbRrpjSvdeE6ofLz4dHmyxD5f7gIdGT4+p36L6Twg==", "dev": true, "license": "MIT", "engines": { @@ -3993,6 +4084,20 @@ "node": ">=18.18.0" } }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -4008,9 +4113,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.0.tgz", + "integrity": "sha512-xnRgu9DxZbkWak/te3fcytNyp8MTbuiZIaueg2rgEvBuN55n04nwLYLU9TX/VVlusc9L2ZNXi99nUFNkHXtr5g==", "dev": true, "license": "Apache-2.0", "engines": { @@ -4021,6 +4126,29 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT", + "optional": true + }, + "node_modules/@iconify/utils": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.1.33.tgz", + "integrity": "sha512-jP9h6v/g0BIZx0p7XGJJVtkVnydtbgTgt9mVNcGDYwaa7UhdHdI9dvoq+gKj9sijMSJKxUPEG2JyjsgXjxL7Kw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@antfu/install-pkg": "^0.4.0", + "@antfu/utils": "^0.7.10", + "@iconify/types": "^2.0.0", + "debug": "^4.3.6", + "kolorist": "^1.8.0", + "local-pkg": "^0.5.0", + "mlly": "^1.7.1" + } + }, "node_modules/@inquirer/checkbox": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", @@ -4584,6 +4712,16 @@ "react": ">=16" } }, + "node_modules/@mermaid-js/parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz", + "integrity": "sha512-HsvL6zgE5sUPGgkIDlmAWR1HTNHz2Iy11BAWPTa4Jjabkpguy4Ze2gzfLrg6pdRuBvFwgUYyxiaNqZwrEEXepA==", + "license": "MIT", + "optional": true, + "dependencies": { + "langium": "3.0.0" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5421,9 +5559,9 @@ } }, "node_modules/@storybook/addon-actions": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.0.tgz", - "integrity": "sha512-xQ84mDIl+jyDpjt8SnCfhqVECQu7k1dLyhiAi983Tp5nyW8KRJa/tEATDLOCpz1eL9AMf2WjAypi+vIiNIul8w==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-8.4.1.tgz", + "integrity": "sha512-D6KohTIA4JCHNol1X7Whp4LpOVU4cS5FfyOorwYo/WIzpHrUYc4Pw/+ex6DOmU/kgrk14mr8d9obVehKW7iNtA==", "dev": true, "license": "MIT", "dependencies": { @@ -5438,7 +5576,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-actions/node_modules/@types/uuid": { @@ -5463,9 +5601,9 @@ } }, "node_modules/@storybook/addon-backgrounds": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.4.0.tgz", - "integrity": "sha512-2LpA7Ja7s76rFjSQHTPhbfmwsCmAuyU5k05CIbbUxM+iBVOaBXUYLaoi8dl448W/o/rmNHeW5YCtxzmMPlScrQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-backgrounds/-/addon-backgrounds-8.4.1.tgz", + "integrity": "sha512-DIT1E9R9Sds8KTC+0m2X5cVa8hTNcKY1XKYTI9QdzQvdZzOt+K93AJqq2x8k5glingqUVpB6v2fSDmCUXp4+4g==", "dev": true, "license": "MIT", "dependencies": { @@ -5478,13 +5616,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-controls": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.4.0.tgz", - "integrity": "sha512-KoqwWHi6cUv1WXcANH4l175kNkuFPVhexP/8F9tE9uhv2xHNx5cTefmB174dWpfOO2H3IdUk0RuMWjOZFpztqQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-controls/-/addon-controls-8.4.1.tgz", + "integrity": "sha512-3ahbYdDx7iFUd4X1KelMSuPqVnladc0bH4m6DQZyN+wkRxdRlOD6iOGuOe2qi1Gv0b2VuVAt253i75tK/TPNLw==", "dev": true, "license": "MIT", "dependencies": { @@ -5497,20 +5635,20 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-docs": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.4.0.tgz", - "integrity": "sha512-n/tAu8xmfdxTkr7ooDM3h+QwDyP9eoKoKuaKXfiPPevrFk0FXRw5KzNhTHTlHniJ2LD+gyaomPGV6D2oBl1KIg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-8.4.1.tgz", + "integrity": "sha512-yPD/NssJf7pMJzaKvma02C6yX8ykPVnEjhRbNYcBNM8s8g/cT5JkROvIB+FOb4T81yhdfbGg9bGkpAXGX270IQ==", "dev": true, "license": "MIT", "dependencies": { "@mdx-js/react": "^3.0.0", - "@storybook/blocks": "8.4.0", - "@storybook/csf-plugin": "8.4.0", - "@storybook/react-dom-shim": "8.4.0", + "@storybook/blocks": "8.4.1", + "@storybook/csf-plugin": "8.4.1", + "@storybook/react-dom-shim": "8.4.1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", "ts-dedent": "^2.0.0" @@ -5520,25 +5658,25 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-essentials": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.4.0.tgz", - "integrity": "sha512-45CI0LpNr8ASHEckxbW/osgnsFMWl847S9rALNQUAN3VaqlDQeF/VIDt1s9vtV9ZYNHASxPFmW4qjgylxv8HpQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-essentials/-/addon-essentials-8.4.1.tgz", + "integrity": "sha512-Hmb5fpVzQgyCacDtHeE7HJqIfolzeOnedsLyJVYVpKns/uOWXqpDuU8Fc0s3yTjr1QPIRKtbqV1STxoyXj2how==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/addon-actions": "8.4.0", - "@storybook/addon-backgrounds": "8.4.0", - "@storybook/addon-controls": "8.4.0", - "@storybook/addon-docs": "8.4.0", - "@storybook/addon-highlight": "8.4.0", - "@storybook/addon-measure": "8.4.0", - "@storybook/addon-outline": "8.4.0", - "@storybook/addon-toolbars": "8.4.0", - "@storybook/addon-viewport": "8.4.0", + "@storybook/addon-actions": "8.4.1", + "@storybook/addon-backgrounds": "8.4.1", + "@storybook/addon-controls": "8.4.1", + "@storybook/addon-docs": "8.4.1", + "@storybook/addon-highlight": "8.4.1", + "@storybook/addon-measure": "8.4.1", + "@storybook/addon-outline": "8.4.1", + "@storybook/addon-toolbars": "8.4.1", + "@storybook/addon-viewport": "8.4.1", "ts-dedent": "^2.0.0" }, "funding": { @@ -5546,13 +5684,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-highlight": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.4.0.tgz", - "integrity": "sha512-tshX/2HnPzGQ9Kza2DARNfirBRhE/Ts7bldbhMiJu20YhJD1jQzXSDEX1cCgHsDc8HKYOsV/Kuu5WDzp/1i97w==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-highlight/-/addon-highlight-8.4.1.tgz", + "integrity": "sha512-BBkUd6+i7lUEWZwoJDlUIwrs7EXkk+EoREUi27iiA1Lilw+NNhoC3kcBmj3+MccjRyeMeIWAgYyXF5qeB2s/JQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5563,19 +5701,19 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-interactions": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.4.0.tgz", - "integrity": "sha512-yXPAyGRjElYZ0ObUo7Ipww4CwgScc2FXMxeQHKSZ+9wuDOU8uSaWpINB++8nS6yPZyhHeUqgzGCF/w3ZusNvzA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.4.1.tgz", + "integrity": "sha512-rMxKehtQogV6Scjb/oqMFM0Mwn8NJRuGFDRJE3TBijNSJ2HPJms+xXp8KVZJengadlsF5HFwQBbnZzIeFDQRLw==", "dev": true, "license": "MIT", "dependencies": { "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.4.0", - "@storybook/test": "8.4.0", + "@storybook/instrumenter": "8.4.1", + "@storybook/test": "8.4.1", "polished": "^4.2.2", "ts-dedent": "^2.2.0" }, @@ -5584,13 +5722,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-links": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.4.0.tgz", - "integrity": "sha512-6MxHHfeshQLA0q40/djK7LrDDLtYt/FnKbNWgH4fbj281IELn1BTYc8cihyN7CZEWyqRqusi6EFpGFgO3LWBgA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-links/-/addon-links-8.4.1.tgz", + "integrity": "sha512-wg83rNKo6mq5apV7f1qMn4q8xZ8wVx/42EEWxTOmnM37Q5kXltEBu+rUyBpPNDU8zBuXr/MRKIhK5h2k4WfWcg==", "dev": true, "license": "MIT", "dependencies": { @@ -5604,7 +5742,7 @@ }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.0" + "storybook": "^8.4.1" }, "peerDependenciesMeta": { "react": { @@ -5613,9 +5751,9 @@ } }, "node_modules/@storybook/addon-measure": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.4.0.tgz", - "integrity": "sha512-Zews/03IL/UUJMaheduGxJKG1mEwfpGq7SP1RtK0kK3l/yh6kVcKG63RXw5zVEoDwG4wzuuH9vi06Mlzhu8/rA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-measure/-/addon-measure-8.4.1.tgz", + "integrity": "sha512-Pg1ROj29hKt7grL/HmbIJ10WrkZf1Unx35SsP373bkPQ1ggYi9oxGqtfNchTF2zCb1xUpIikLYSJgkwdjqWxhA==", "dev": true, "license": "MIT", "dependencies": { @@ -5627,13 +5765,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-outline": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.4.0.tgz", - "integrity": "sha512-qZdHaWq/DXoVycKzcynvVxg3MNzavsGCuq9HUl2X/oBKNii00NEZgYVLo4dQ8iDNlmykuJ9ReyXKBOKF7AU+9w==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-outline/-/addon-outline-8.4.1.tgz", + "integrity": "sha512-LPZ0gGHfbru66Lkw1whnc3F/r1hfnoORBoF98Hp+cjH34gR4t8te6xq5qSiupRUULGdSLdBRs/4EGRBeELfVjQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5645,13 +5783,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-toolbars": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.4.0.tgz", - "integrity": "sha512-fXDeLsAweC1/roe5qNys+pBrjf1Mxof/7O/dZtQZJtcKox4WwzgirxexFFAZLfXOE9awm5svzo0YWYxWk+Lfwg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.4.1.tgz", + "integrity": "sha512-yrzX6BFeJM5KFY0+ZAYfRax2QgWi2e5vF6yPz+MGIPr4nhHay0wTkOHhkBhIPBjQO9x0vqc7MS2EBDydCBWqlg==", "dev": true, "license": "MIT", "funding": { @@ -5659,13 +5797,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/addon-viewport": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.4.0.tgz", - "integrity": "sha512-hbHJzz7PcZ/bazUH3nAdG9yP3CUfF+wPdDwzcqSEVBRjdWSLZ4DHAtB0wajqhUoCsiRehg9avft1NokAc+KOgg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-viewport/-/addon-viewport-8.4.1.tgz", + "integrity": "sha512-O6DcuUfXQTytjl7mj4ld4ZX9x2pUUWKUx1TxiuMuH0EKb612RyYcdpXpDQQwsIzLV/f2BOetk9jmO2/MymfbWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5676,23 +5814,23 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/angular": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-8.4.0.tgz", - "integrity": "sha512-e1V4x18MvGaxLfGIRq3cUGkFqUR0tc6MZ7FAu/DQOgl6aJ9P8YAthVot9fYu8i2uY3Zb/dOWWNJt4zqw7D806w==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/angular/-/angular-8.4.1.tgz", + "integrity": "sha512-UVegOQv1w7KAROa7QqBe4g4xJ01lSKbF7ML2jfEL02c+NQ/sLvlEeziAy0R0jOr5/LE1q+zv6t8F2yqN67XVqw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/builder-webpack5": "8.4.0", - "@storybook/components": "^8.4.0", - "@storybook/core-webpack": "8.4.0", + "@storybook/builder-webpack5": "8.4.1", + "@storybook/components": "8.4.1", + "@storybook/core-webpack": "8.4.1", "@storybook/global": "^5.0.0", - "@storybook/manager-api": "^8.4.0", - "@storybook/preview-api": "^8.4.0", - "@storybook/theming": "^8.4.0", + "@storybook/manager-api": "8.4.1", + "@storybook/preview-api": "8.4.1", + "@storybook/theming": "8.4.1", "@types/node": "^22.0.0", "@types/react": "^18.0.37", "@types/react-dom": "^18.0.11", @@ -5727,7 +5865,7 @@ "@angular/platform-browser": ">=15.0.0 < 19.0.0", "@angular/platform-browser-dynamic": ">=15.0.0 < 19.0.0", "rxjs": "^6.0.0 || ^7.4.0", - "storybook": "^8.4.0", + "storybook": "^8.4.1", "typescript": "^4.0.0 || ^5.0.0", "zone.js": ">= 0.11.1 < 1.0.0" }, @@ -5738,9 +5876,9 @@ } }, "node_modules/@storybook/blocks": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.0.tgz", - "integrity": "sha512-LeXsZLTNcmKtgt0ZRdgzBa2Z8A5CH3gGyjG7QT3M+3yH9fVAXB2XplKOIejDsvR9jSBww3mKXyabX12NVZKz0A==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/blocks/-/blocks-8.4.1.tgz", + "integrity": "sha512-C4w5T5fhg0iONXozHQ1bh9im2Lr1BiY7Bj/9XoFjkc5YeCzxlMpujFA6Nmo4ToUFW90QbvKN7/QVhbrtY9O1Jg==", "dev": true, "license": "MIT", "dependencies": { @@ -5755,7 +5893,7 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.0" + "storybook": "^8.4.1" }, "peerDependenciesMeta": { "react": { @@ -5767,13 +5905,13 @@ } }, "node_modules/@storybook/builder-webpack5": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.4.0.tgz", - "integrity": "sha512-NVPEB31x1LU73ghgPaynY603Pi0MKPlM/YovevlwZtTIU9st+DSEss1qSjC0As2Lq/bHZTJu+jhTCIB76MK7wQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-8.4.1.tgz", + "integrity": "sha512-rqSJcxcYiQyceNFSrT9qnI6hrW4/petb1n+oN8nG5HrRsl0zxOVzamMVyNzZxrAMKvq+VMJtLe1rQi8FnJNunw==", "dev": true, "license": "MIT", "dependencies": { - "@storybook/core-webpack": "8.4.0", + "@storybook/core-webpack": "8.4.1", "@types/node": "^22.0.0", "@types/semver": "^7.3.4", "browser-assert": "^1.2.1", @@ -5804,7 +5942,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" }, "peerDependenciesMeta": { "typescript": { @@ -5878,9 +6016,9 @@ } }, "node_modules/@storybook/components": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.0.tgz", - "integrity": "sha512-o2jPW05YN2rbSLNMzPV769c4zCy3Vn0DhJbIQZsxUmUXAMX/n1+V1jlV3kbY0kCjiI6i/PH7i6PJnxICdJ35mQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-8.4.1.tgz", + "integrity": "sha512-bMPclbBhrWxhFlwqrC/h4fPLl05ouoi5D8SkQTHjeVxWN9eDnMVi76xM0YDct302Z3f0x5S3plIulp+4XRxrvg==", "dev": true, "license": "MIT", "funding": { @@ -5888,13 +6026,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/core": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.4.0.tgz", - "integrity": "sha512-RlvkBNPPLbHtJQ5M3SKfLLtn5GssRBOLBbJLJf8HjraeDI+YRt+J9FVXqNa9aHhOGoxam+hFinmuy9gyMbPW1A==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/core/-/core-8.4.1.tgz", + "integrity": "sha512-q3Q4OFBj7MHHbIFYk/Beejlqv5j7CC3+VWhGcr0TK3SGvdCIZ7EliYuc5JIOgDlEPsnTIk+lkgWI4LAA9mLzSw==", "license": "MIT", "dependencies": { "@storybook/csf": "^0.1.11", @@ -5923,9 +6061,9 @@ } }, "node_modules/@storybook/core-webpack": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.4.0.tgz", - "integrity": "sha512-14UnJ7zFSLEyaBvYe7+K1t/TWJc41KxstMHgVxHyE6TDy9MGi+GLfmq2xB5OIVE4nxtjSon3tIOf/hVBrtbt0A==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/core-webpack/-/core-webpack-8.4.1.tgz", + "integrity": "sha512-TptbDGaj9a8wJMF4g+C8t02CXl4BSd0BA/qGWBvzn3j4FJqeQ/m8elOXLYZrPbQKI6PjP0J4ayHkXdX2h0/tUw==", "dev": true, "license": "MIT", "dependencies": { @@ -5937,7 +6075,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/csf": { @@ -5950,9 +6088,9 @@ } }, "node_modules/@storybook/csf-plugin": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.4.0.tgz", - "integrity": "sha512-l4vD1XboHh3nFOvcCIjoTED6bQZtRx+T/CUFfuZu3KEA7uJnXt/kUCXair9+Cgky9XvSEMvBPhoqa2dRx9ibBQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-8.4.1.tgz", + "integrity": "sha512-MdQkyq6mJ31lBsWCG9VNtx8O0oLSc5h4kvWDPyIP6Dn58K0Hv2z9qvxxSvtFjXA7ES9X+ivjorTke1kearifhg==", "dev": true, "license": "MIT", "dependencies": { @@ -5963,7 +6101,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/global": { @@ -5988,9 +6126,9 @@ } }, "node_modules/@storybook/instrumenter": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.4.0.tgz", - "integrity": "sha512-iqQdH2lhyRVcCBnVOmjn/r/pFwIJ5X1isUkvyavwPf0KOB2bz+QuXXkvKdzirwQFu9jSLOEdu0v3Fr+PHUbIfA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.4.1.tgz", + "integrity": "sha512-MgrhrLVW78jqno+Dh9h9Es06Ja3867TlrIUd8B3K3U1hsCFUQuFKXJBuGjNJF8U0QJY/aSIRnAgUBurHdVkPcw==", "dev": true, "license": "MIT", "dependencies": { @@ -6002,39 +6140,39 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/manager-api": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.0.tgz", - "integrity": "sha512-duYoAtx3VkTHpoXd+NaMqBQNqIovmbTN7w/244O0LWyhF6AmQXnrY1Z72rjvvpxY6c1boRs6YdDLXPKxGVeRxw==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.4.1.tgz", + "integrity": "sha512-7hb2k4zsp6lREGZbQ85QOlsC8EIMZXuY9Pg12VUgaZd+LmLjLuaqtrxRz3SwIgIWsRpFun9AHO0X37DmYNGTSw==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/preview-api": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.0.tgz", - "integrity": "sha512-Z9yduQRqzqeV85GEFyaTKtRtg/QYCb89bKhi4xcxY9l7DMAr7/lqpUxqngW5ogiNslusQzct3zI7os6INBlMFg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/preview-api/-/preview-api-8.4.1.tgz", + "integrity": "sha512-VdnESYfXCUasNtMd5s1Q8DPqMnAUdpROn8mE8UAD79Cy7DSNesI1q0SATuJqh5iYCT/+3Tpjfghsr2zC/mOh8w==", "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@storybook/react-dom-shim": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.0.tgz", - "integrity": "sha512-PYYZVdQ6/ts6hBMAwMEu4hfbyHFPzUYmVsZNtF2egaVJQ44xM4i1Zt+RJuo2NOt5VyBCfXJOs+lSIdmSBY2arw==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-8.4.1.tgz", + "integrity": "sha512-XhvuqkpqtcUjDA8XE4osq140SCddX3VHMdj+IwlrMdoSl32CAya01TH5YDDx6YMy6hM/QQbyVKaemG7RB/oU4Q==", "dev": true, "license": "MIT", "funding": { @@ -6044,19 +6182,19 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/test": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.4.0.tgz", - "integrity": "sha512-uHZ6+8RfEauwxi7Zy/LijfyIXrjCD7iTHmnTdT3BdP+2c/lDFAKXzHmbQJitefDFEgz1eHx/MArHZ8V3qu1ogg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.4.1.tgz", + "integrity": "sha512-najn9kCxB8NaHykhD7Fv+Iq0FnxmIJYOJlYiI8NMgVLwaSDFf6gnqAY6HHVPRqkhej8TuT1L2e2RxKqzWEB+mA==", "dev": true, "license": "MIT", "dependencies": { "@storybook/csf": "^0.1.11", "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.4.0", + "@storybook/instrumenter": "8.4.1", "@testing-library/dom": "10.4.0", "@testing-library/jest-dom": "6.5.0", "@testing-library/user-event": "14.5.2", @@ -6068,13 +6206,13 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.4.1" } }, "node_modules/@storybook/theming": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.0.tgz", - "integrity": "sha512-S7Iv5HMiYEJZlkQM0K9bxACLN7s8lCSG3M2CN6A82LSoXayFauuaPpn3LrNE2BvkTpdu17w19YiGbVYhPtRqsg==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.4.1.tgz", + "integrity": "sha512-Sz24isryVFZaVahXkjgnCsMAQqQeeKg41AtLsldlYdesIo6fr5tc6/SkTUy+CYadK4Dkhqp+vVRDnwToYYRGhA==", "dev": true, "license": "MIT", "funding": { @@ -6082,7 +6220,7 @@ "url": "https://opencollective.com/storybook" }, "peerDependencies": { - "storybook": "^8.4.0" + "storybook": "^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0" } }, "node_modules/@testing-library/dom": { @@ -6403,143 +6541,444 @@ "@types/node": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", "license": "MIT", + "optional": true, "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT", + "optional": true }, - "node_modules/@types/eslint__js": { - "version": "8.42.3", - "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", - "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", - "dev": true, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", "license": "MIT", + "optional": true, "dependencies": { - "@types/eslint": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/express": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", - "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", - "dev": true, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", "license": "MIT", + "optional": true, "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", - "dev": true, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } + "optional": true }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "dev": true, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", "license": "MIT", + "optional": true, "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" + "@types/d3-array": "*", + "@types/geojson": "*" } }, - "node_modules/@types/file-saver": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", - "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "dev": true, - "license": "MIT" + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT", + "optional": true }, - "node_modules/@types/http-errors": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true, - "license": "MIT" + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT", + "optional": true }, - "node_modules/@types/http-proxy": { - "version": "1.17.15", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", - "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", - "dev": true, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", "license": "MIT", + "optional": true, "dependencies": { - "@types/node": "*" + "@types/d3-selection": "*" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "dev": true, - "license": "MIT" + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT", + "optional": true }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT", + "optional": true }, - "node_modules/@types/mute-stream": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", - "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", - "dev": true, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", "license": "MIT", + "optional": true, "dependencies": { - "@types/node": "*" + "@types/d3-dsv": "*" } }, - "node_modules/@types/node": { - "version": "22.8.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.6.tgz", - "integrity": "sha512-tosuJYKrIqjQIlVCM4PEGxOmyg3FCPa/fViuJChnGeEIhjA46oy8FMVoF9su1/v8PNs2a8Q0iFNyOx0uOF91nw==", - "devOptional": true, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", "license": "MIT", + "optional": true, "dependencies": { - "undici-types": "~6.19.8" + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", + "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", + "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/html-minifier-terser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", + "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.8.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.7.tgz", + "integrity": "sha512-LidcG+2UeYIWcMuMUpBKOnryBWG/rnmOHQR5apjn8myTQcx3rinFRn7DcIFhMnS0PPFSC6OafdIKEad0lj6U0Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" } }, "node_modules/@types/node-forge": { @@ -6658,6 +7097,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -6680,9 +7126,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", "dev": true, "license": "MIT", "dependencies": { @@ -7208,7 +7654,7 @@ "version": "8.14.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -8363,9 +8809,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001676", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001676.tgz", - "integrity": "sha512-Qz6zwGCiPghQXGJvgQAem79esjitvJ+CxSbSQkW9H/UX5hg8XM88d4lp2W+MEQ81j+Hip58Il+jGVdazk1z9cw==", + "version": "1.0.30001677", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001677.tgz", + "integrity": "sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==", "dev": true, "funding": [ { @@ -8484,6 +8930,34 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -8770,6 +9244,18 @@ "node": ">= 12" } }, + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "license": "MIT", + "optional": true, + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -9047,6 +9533,13 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "license": "MIT" }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT", + "optional": true + }, "node_modules/connect": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", @@ -9247,6 +9740,16 @@ "node": ">= 0.10" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", @@ -9397,18 +9900,589 @@ "dev": true, "license": "MIT" }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "node_modules/cytoscape": { + "version": "3.30.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.3.tgz", + "integrity": "sha512-HncJ9gGJbVtw7YXtIs3+6YAFSSiKsom0amWc33Z7QbylbY2JGMrA0yz4EwrdTScZxnwclXeEZHzO5pxoy0ZE4g==", "license": "MIT", + "optional": true, "engines": { - "node": ">= 14" + "node": ">=0.10" } }, - "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "optional": true, + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT", + "optional": true + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "optional": true, + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "optional": true, + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-dsv/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC", + "optional": true + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "optional": true, + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "license": "MIT", + "optional": true, + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "optional": true + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "license": "MIT", "dependencies": { @@ -9582,6 +10656,16 @@ "node": ">= 14" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "optional": true, + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -9591,6 +10675,13 @@ "node": ">=0.4.0" } }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9650,7 +10741,17 @@ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } }, "node_modules/dlv": { "version": "1.1.3", @@ -9733,6 +10834,13 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true + }, "node_modules/domutils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", @@ -9808,6 +10916,13 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/emoji-toolkit": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/emoji-toolkit/-/emoji-toolkit-9.0.1.tgz", + "integrity": "sha512-sMMNqKNLVHXJfIKoPbrRJwtYuysVNC9GlKetr72zE3SSVbHqoeDLWVrxP0uM0AE0qvdl3hbUk+tJhhwXZrDHaw==", + "license": "MIT", + "optional": true + }, "node_modules/emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", @@ -10099,22 +11214,22 @@ } }, "node_modules/eslint": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz", - "integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.14.0.tgz", + "integrity": "sha512-c2FHsVBr87lnUtjP4Yhvk4yEhKrQavGafRA/Se1ouse8PfbfC/Qh9Mxa00yWsZRlqeUB9raXip0aiiUZkgnr9g==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", + "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.18.0", "@eslint/core": "^0.7.0", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.13.0", + "@eslint/js": "9.14.0", "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.0", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", @@ -10122,9 +11237,9 @@ "cross-spawn": "^7.0.2", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -11535,6 +12650,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==", + "license": "MIT", + "optional": true, + "dependencies": { + "delegate": "^3.1.2" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -11560,6 +12685,13 @@ "dev": true, "license": "MIT" }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT", + "optional": true + }, "node_modules/hammerjs": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz", @@ -12247,6 +13379,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "optional": true, + "engines": { + "node": ">=12" + } + }, "node_modules/ip-address": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", @@ -12890,6 +14032,33 @@ "source-map-support": "^0.5.5" } }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 12" + } + }, "node_modules/keycharm": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.2.0.tgz", @@ -12906,6 +14075,12 @@ "json-buffer": "3.0.1" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "optional": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -12915,6 +14090,30 @@ "node": ">=0.10.0" } }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT", + "optional": true + }, + "node_modules/langium": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.0.0.tgz", + "integrity": "sha512-+Ez9EoiByeoTu/2BXmEaZ06iPNXM6thWJp02KfBO/raSMyCJ4jw7AkWWa+zBCTm0+Tw1Fj9FOxdqSskyN5nAwg==", + "license": "MIT", + "optional": true, + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/launch-editor": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", @@ -12926,6 +14125,13 @@ "shell-quote": "^1.8.1" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT", + "optional": true + }, "node_modules/less": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", @@ -13195,6 +14401,23 @@ "node": ">= 12.13.0" } }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "license": "MIT", + "optional": true, + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -13217,6 +14440,13 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT", + "optional": true + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -13589,16 +14819,16 @@ } }, "node_modules/marked": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.3.tgz", - "integrity": "sha512-ev2uM40p0zQ/GbvqotfKcSWEa59fJwluGZj5dcaUOwDRrB1F3dncdXy8NWUApk4fi8atU3kTBOwjyjZ0ud0dxw==", - "dev": true, + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 16" + "node": ">= 18" } }, "node_modules/media-typer": { @@ -13661,6 +14891,63 @@ "node": ">= 8" } }, + "node_modules/mermaid": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.0.tgz", + "integrity": "sha512-mxCfEYvADJqOiHfGpJXLs4/fAjHz448rH0pfY5fAoxiz70rQiDSzUUy4dNET2T08i46IVpjohPd6WWbzmRHiPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^7.0.1", + "@iconify/utils": "^2.1.32", + "@mermaid-js/parser": "^0.3.0", + "@types/d3": "^7.4.3", + "@types/dompurify": "^3.0.5", + "cytoscape": "^3.29.2", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.10", + "dompurify": "^3.0.11 <3.1.7", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^13.0.2", + "roughjs": "^4.6.6", + "stylis": "^4.3.1", + "ts-dedent": "^2.2.0", + "uuid": "^9.0.1" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/marked/-/marked-13.0.3.tgz", + "integrity": "sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA==", + "license": "MIT", + "optional": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mermaid/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -14030,6 +15317,19 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/mlly": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.2.tgz", + "integrity": "sha512-tN3dvVHYVz4DhSXinXIk7u9syPYaJvio118uomkovAtWBT+RdbP6Lfh/5Lvo519YMmwBafwlh20IPTXIStscpA==", + "license": "MIT", + "optional": true, + "dependencies": { + "acorn": "^8.12.1", + "pathe": "^1.1.2", + "pkg-types": "^1.2.0", + "ufo": "^1.5.4" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -14281,6 +15581,30 @@ "node": ">= 0.4.0" } }, + "node_modules/ngx-markdown": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/ngx-markdown/-/ngx-markdown-18.1.0.tgz", + "integrity": "sha512-n4HFSm5oqVMXFuD+WXIVkI6NyxD8Oubr4B3c9U1J7Ptr6t9DVnkNBax3yxWc+8Wli+FXTuGEnDXzB3sp7E9paA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "clipboard": "^2.0.11", + "emoji-toolkit": ">= 8.0.0 < 10.0.0", + "katex": "^0.16.0", + "mermaid": ">= 10.6.0 < 12.0.0", + "prismjs": "^1.28.0" + }, + "peerDependencies": { + "@angular/common": "^18.0.0", + "@angular/core": "^18.0.0", + "@angular/platform-browser": "^18.0.0", + "marked": ">= 9.0.0 < 13.0.0", + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, "node_modules/ngx-skeleton-loader": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-9.0.0.tgz", @@ -14759,9 +16083,9 @@ } }, "node_modules/npm-check-updates": { - "version": "17.1.9", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.9.tgz", - "integrity": "sha512-Gfv5S8NNJKTilM1gesFNYka6bUaBs5LnVyPjApXPQphHijrlLFDMw1uSmwYMZbvJSkLZSOx03e8CHcG0Td5SMA==", + "version": "17.1.10", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.10.tgz", + "integrity": "sha512-GnN6KbUzC8BpwsRYJntuumgCiagZ0+xxorvUJM9m06d7AlyK9lm3iFsAsnXF3VAZZzpD5QjZvWBwNze61Vywkw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -17904,9 +19228,9 @@ "license": "ISC" }, "node_modules/ordered-binary": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.2.tgz", - "integrity": "sha512-JTo+4+4Fw7FreyAvlSLjb1BBVaxEQAacmjD3jjuyPZclpbEghTvQZbXBb2qPd2LeIMxiHwXBZUcpmG2Gl/mDEA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", "dev": true, "license": "MIT" }, @@ -18060,6 +19384,13 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/package-manager-detector": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.2.tgz", + "integrity": "sha512-VgXbyrSNsml4eHWIvxxG/nTL4wgybMTXCV2Un/+yEc3aDKKU6nQBZjbeP3Pl3qm9Qg92X/1ng4ffvCeD/zwHgg==", + "license": "MIT", + "optional": true + }, "node_modules/pacote": { "version": "18.0.6", "resolved": "https://registry.npmjs.org/pacote/-/pacote-18.0.6.tgz", @@ -18241,6 +19572,13 @@ "dev": true, "license": "MIT" }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT", + "optional": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -18322,6 +19660,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "license": "MIT", + "optional": true + }, "node_modules/pathval": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", @@ -18505,6 +19850,36 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pkg-types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.2.1.tgz", + "integrity": "sha512-sQoqa8alT3nHjGuTjuKgOnvjo4cljkufdtLMnO2LBP/wRwuDlo1tkaEdMxCRhyGRPacv/ztlZgDPm2b7FAmEvw==", + "license": "MIT", + "optional": true, + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.2", + "pathe": "^1.1.2" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT", + "optional": true + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "optional": true, + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/polished": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/polished/-/polished-4.3.1.tgz", @@ -18818,6 +20193,45 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/prettier-plugin-classnames": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/prettier-plugin-classnames/-/prettier-plugin-classnames-0.7.4.tgz", + "integrity": "sha512-QDhISdUeYcwHHtsHBs+xImIeT+6DObStqxO1Aouv6biBXpdXa9OfOjeGiJ+GdcWYfN47WTbdcBKjFqYgflRVYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "prettier": "^2 || ^3", + "prettier-plugin-astro": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/prettier-plugin-merge": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/prettier-plugin-merge/-/prettier-plugin-merge-0.7.1.tgz", + "integrity": "sha512-R3dSlv3kAlScjd/liWjTkGHcUrE4MBhPKKBxVOvHK7+FY2P5SEmLarZiD11VUEuaMRK0L7zqIurX6JcRYS9Y5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff": "5.1.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + } + }, "node_modules/prettier-plugin-tailwindcss": { "version": "0.6.8", "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz", @@ -18940,7 +20354,7 @@ "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6" @@ -19827,6 +21241,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense", + "optional": true + }, "node_modules/rollup": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", @@ -19870,6 +21291,19 @@ "dev": true, "license": "MIT" }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -19907,6 +21341,13 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause", + "optional": true + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -20063,6 +21504,13 @@ } } }, + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==", + "license": "MIT", + "optional": true + }, "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -20963,12 +22411,12 @@ } }, "node_modules/storybook": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.0.tgz", - "integrity": "sha512-hLfXPtqfoQUMKVortxXdnQoUwDwtH85eSj9LbqGT/z1f/gLLYGNG3Mv3QbsRjHXhn+EfYffh7wuLpAn+Cicijw==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-8.4.1.tgz", + "integrity": "sha512-0tfFIFghjho9FtnFoiJMoxhcs2iIdvEF81GTSVnTsDVJrYA84nB+FxN3UY1fT0BcQ8BFlbf+OhSjZL7ufqqWKA==", "license": "MIT", "dependencies": { - "@storybook/core": "8.4.0" + "@storybook/core": "8.4.1" }, "bin": { "getstorybook": "bin/index.cjs", @@ -21778,6 +23226,13 @@ "webpack": "^5.0.0" } }, + "node_modules/stylis": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.4.tgz", + "integrity": "sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==", + "license": "MIT", + "optional": true + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -22255,12 +23710,26 @@ "dev": true, "license": "MIT" }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT", + "optional": true + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, + "node_modules/tinyexec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.1.tgz", + "integrity": "sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==", + "license": "MIT", + "optional": true + }, "node_modules/tinyglobby": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.10.tgz", @@ -22449,7 +23918,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=6.10" @@ -22614,6 +24083,13 @@ } } }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "license": "MIT", + "optional": true + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -23526,6 +25002,61 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "optional": true, + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "optional": true, + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT", + "optional": true + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT", + "optional": true + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT", + "optional": true + }, "node_modules/wait-on": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 209498bde8..4ead18c1e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "highlight.js": "^11.10.0", "http-status-codes": "^2.3.0", "monaco-editor": "^0.52.0", + "ngx-markdown": "^18.1.0", "ngx-skeleton-loader": "^9.0.0", "ngx-toastr": "^19.0.0", "npm": "^10.9.0", @@ -75,6 +76,8 @@ "npm-check-updates": "^17.1.9", "postcss": "^8.4.47", "prettier": "^3.3.3", + "prettier-plugin-classnames": "^0.7.4", + "prettier-plugin-merge": "^0.7.1", "prettier-plugin-tailwindcss": "^0.6.8", "storybook": "^8.4.0", "tailwindcss": "^3.4.14", diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 4cbddf5a64..51c5085fee 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -11,6 +11,7 @@ import { PipelineRunWrapperComponent } from 'src/app/projects/models/backup-sett import { ViewLogsDialogComponent } from 'src/app/projects/models/backup-settings/view-logs-dialog/view-logs-dialog.component'; import { PipelineWrapperComponent } from 'src/app/projects/models/backup-settings/wrapper/pipeline-wrapper/pipeline-wrapper.component'; import { ModelRestrictionsComponent } from 'src/app/projects/models/model-restrictions/model-restrictions.component'; +import { CreateProjectToolsComponent } from 'src/app/projects/project-detail/create-project-tools/create-project-tools.component'; import { EditProjectMetadataComponent } from 'src/app/projects/project-detail/edit-project-metadata/edit-project-metadata.component'; import { SessionComponent } from 'src/app/sessions/session/session.component'; import { ConfigurationSettingsComponent } from 'src/app/settings/core/configuration-settings/configuration-settings.component'; @@ -107,6 +108,16 @@ export const routes: Routes = [ }, component: EditProjectMetadataComponent, }, + { + path: 'tools', + children: [ + { + path: 'link', + data: { breadcrumb: 'Link Tool' }, + component: CreateProjectToolsComponent, + }, + ], + }, { path: 'models', children: [ diff --git a/frontend/src/app/general/footer/footer.component.html b/frontend/src/app/general/footer/footer.component.html index 20345c7aa0..88d4548c1c 100644 --- a/frontend/src/app/general/footer/footer.component.html +++ b/frontend/src/app/general/footer/footer.component.html @@ -5,7 +5,8 @@
diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES index a55073549f..a3848cb093 100644 --- a/frontend/src/app/openapi/.openapi-generator/FILES +++ b/frontend/src/app/openapi/.openapi-generator/FILES @@ -14,9 +14,12 @@ api/projects-models-backups.service.ts api/projects-models-diagrams.service.ts api/projects-models-git.service.ts api/projects-models-model-complexity-badge.service.ts +api/projects-models-provisioning.service.ts +api/projects-models-readme.service.ts api/projects-models-restrictions.service.ts api/projects-models-t4-c.service.ts api/projects-models.service.ts +api/projects-tools.service.ts api/projects.service.ts api/sessions.service.ts api/settings-modelsources-git.service.ts @@ -90,6 +93,7 @@ model/minimal-tool-session-connection-method.ts model/minimal-tool-version-with-tool.ts model/minimal-tool.ts model/model-artifact-status.ts +model/model-provisioning.ts model/models.ts model/navbar-configuration-input-external-links-inner.ts model/navbar-configuration-input.ts @@ -119,12 +123,14 @@ model/pipeline-run.ts model/post-git-instance.ts model/post-git-model.ts model/post-project-request.ts +model/post-project-tool-request.ts model/post-project-user.ts model/post-session-request.ts model/post-token.ts model/post-tool-model.ts model/post-user.ts model/project-status.ts +model/project-tool.ts model/project-type.ts model/project-user-permission.ts model/project-user-role.ts @@ -160,6 +166,7 @@ model/simple-t4-c-model-with-repository.ts model/simple-t4-c-model-with-tool-model.ts model/simple-t4-c-repository-with-integrations.ts model/simple-t4-c-repository.ts +model/simple-tool-model-without-project.ts model/simple-tool-model.ts model/simple-tool-version.ts model/status-response.ts diff --git a/frontend/src/app/openapi/api/api.ts b/frontend/src/app/openapi/api/api.ts index 9990dc29f7..b6ce03b8da 100644 --- a/frontend/src/app/openapi/api/api.ts +++ b/frontend/src/app/openapi/api/api.ts @@ -41,10 +41,16 @@ export * from './projects-models-git.service'; import { ProjectsModelsGitService } from './projects-models-git.service'; export * from './projects-models-model-complexity-badge.service'; import { ProjectsModelsModelComplexityBadgeService } from './projects-models-model-complexity-badge.service'; +export * from './projects-models-provisioning.service'; +import { ProjectsModelsProvisioningService } from './projects-models-provisioning.service'; +export * from './projects-models-readme.service'; +import { ProjectsModelsREADMEService } from './projects-models-readme.service'; export * from './projects-models-restrictions.service'; import { ProjectsModelsRestrictionsService } from './projects-models-restrictions.service'; export * from './projects-models-t4-c.service'; import { ProjectsModelsT4CService } from './projects-models-t4-c.service'; +export * from './projects-tools.service'; +import { ProjectsToolsService } from './projects-tools.service'; export * from './sessions.service'; import { SessionsService } from './sessions.service'; export * from './settings-modelsources-git.service'; @@ -63,4 +69,4 @@ export * from './users-token.service'; import { UsersTokenService } from './users-token.service'; export * from './users-workspaces.service'; import { UsersWorkspacesService } from './users-workspaces.service'; -export const APIS = [AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, MetadataService, NavbarService, NoticesService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CInstancesService, SettingsModelsourcesT4CLicenseServersService, ToolsService, UsersService, UsersSessionsService, UsersTokenService, UsersWorkspacesService]; +export const APIS = [AuthenticationService, ConfigurationService, EventsService, FeedbackService, HealthService, IntegrationsPureVariantsService, MetadataService, NavbarService, NoticesService, ProjectsService, ProjectsEventsService, ProjectsModelsService, ProjectsModelsBackupsService, ProjectsModelsDiagramsService, ProjectsModelsGitService, ProjectsModelsModelComplexityBadgeService, ProjectsModelsProvisioningService, ProjectsModelsREADMEService, ProjectsModelsRestrictionsService, ProjectsModelsT4CService, ProjectsToolsService, SessionsService, SettingsModelsourcesGitService, SettingsModelsourcesT4CInstancesService, SettingsModelsourcesT4CLicenseServersService, ToolsService, UsersService, UsersSessionsService, UsersTokenService, UsersWorkspacesService]; diff --git a/frontend/src/app/openapi/api/projects-models-provisioning.service.ts b/frontend/src/app/openapi/api/projects-models-provisioning.service.ts new file mode 100644 index 0000000000..6856a854b6 --- /dev/null +++ b/frontend/src/app/openapi/api/projects-models-provisioning.service.ts @@ -0,0 +1,248 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { HTTPValidationError } from '../model/http-validation-error'; +// @ts-ignore +import { ModelProvisioning } from '../model/model-provisioning'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ProjectsModelsProvisioningService { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * Get Provisioning + * @param projectSlug + * @param modelSlug + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getProvisioning(projectSlug: string, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getProvisioning(projectSlug: string, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProvisioning(projectSlug: string, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProvisioning(projectSlug: string, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (projectSlug === null || projectSlug === undefined) { + throw new Error('Required parameter projectSlug was null or undefined when calling getProvisioning.'); + } + if (modelSlug === null || modelSlug === undefined) { + throw new Error('Required parameter modelSlug was null or undefined when calling getProvisioning.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/provisioning`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Reset Provisioning + * This will delete the provisioning data from the workspace. During the next session request, the existing provisioning will be overwritten in the workspace. + * @param projectSlug + * @param modelSlug + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public resetProvisioning(projectSlug: string, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public resetProvisioning(projectSlug: string, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public resetProvisioning(projectSlug: string, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public resetProvisioning(projectSlug: string, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (projectSlug === null || projectSlug === undefined) { + throw new Error('Required parameter projectSlug was null or undefined when calling resetProvisioning.'); + } + if (modelSlug === null || modelSlug === undefined) { + throw new Error('Required parameter modelSlug was null or undefined when calling resetProvisioning.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/provisioning`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/frontend/src/app/openapi/api/projects-models-readme.service.ts b/frontend/src/app/openapi/api/projects-models-readme.service.ts new file mode 100644 index 0000000000..e2790b8fd9 --- /dev/null +++ b/frontend/src/app/openapi/api/projects-models-readme.service.ts @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { HTTPValidationError } from '../model/http-validation-error'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ProjectsModelsREADMEService { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * Get Readme + * @param projectSlug + * @param modelSlug + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getReadme(projectSlug: string, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/markdown' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getReadme(projectSlug: string, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/markdown' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getReadme(projectSlug: string, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'text/markdown' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getReadme(projectSlug: string, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'text/markdown' | 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (projectSlug === null || projectSlug === undefined) { + throw new Error('Required parameter projectSlug was null or undefined when calling getReadme.'); + } + if (modelSlug === null || modelSlug === undefined) { + throw new Error('Required parameter modelSlug was null or undefined when calling getReadme.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'text/markdown', + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/readme`; + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/frontend/src/app/openapi/api/projects-tools.service.ts b/frontend/src/app/openapi/api/projects-tools.service.ts new file mode 100644 index 0000000000..1ea41b20fb --- /dev/null +++ b/frontend/src/app/openapi/api/projects-tools.service.ts @@ -0,0 +1,330 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +/* tslint:disable:no-unused-variable member-ordering */ + +import { Inject, Injectable, Optional } from '@angular/core'; +import { HttpClient, HttpHeaders, HttpParams, + HttpResponse, HttpEvent, HttpParameterCodec, HttpContext + } from '@angular/common/http'; +import { CustomHttpParameterCodec } from '../encoder'; +import { Observable } from 'rxjs'; + +// @ts-ignore +import { HTTPValidationError } from '../model/http-validation-error'; +// @ts-ignore +import { PostProjectToolRequest } from '../model/post-project-tool-request'; +// @ts-ignore +import { ProjectTool } from '../model/project-tool'; + +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS } from '../variables'; +import { Configuration } from '../configuration'; + + + +@Injectable({ + providedIn: 'root' +}) +export class ProjectsToolsService { + + protected basePath = 'http://localhost'; + public defaultHeaders = new HttpHeaders(); + public configuration = new Configuration(); + public encoder: HttpParameterCodec; + + constructor(protected httpClient: HttpClient, @Optional()@Inject(BASE_PATH) basePath: string|string[], @Optional() configuration: Configuration) { + if (configuration) { + this.configuration = configuration; + } + if (typeof this.configuration.basePath !== 'string') { + const firstBasePath = Array.isArray(basePath) ? basePath[0] : undefined; + if (firstBasePath != undefined) { + basePath = firstBasePath; + } + + if (typeof basePath !== 'string') { + basePath = this.basePath; + } + this.configuration.basePath = basePath; + } + this.encoder = this.configuration.encoder || new CustomHttpParameterCodec(); + } + + + // @ts-ignore + private addToHttpParams(httpParams: HttpParams, value: any, key?: string): HttpParams { + if (typeof value === "object" && value instanceof Date === false) { + httpParams = this.addToHttpParamsRecursive(httpParams, value); + } else { + httpParams = this.addToHttpParamsRecursive(httpParams, value, key); + } + return httpParams; + } + + private addToHttpParamsRecursive(httpParams: HttpParams, value?: any, key?: string): HttpParams { + if (value == null) { + return httpParams; + } + + if (typeof value === "object") { + if (Array.isArray(value)) { + (value as any[]).forEach( elem => httpParams = this.addToHttpParamsRecursive(httpParams, elem, key)); + } else if (value instanceof Date) { + if (key != null) { + httpParams = httpParams.append(key, (value as Date).toISOString().substring(0, 10)); + } else { + throw Error("key may not be null if value is Date"); + } + } else { + Object.keys(value).forEach( k => httpParams = this.addToHttpParamsRecursive( + httpParams, value[k], key != null ? `${key}.${k}` : k)); + } + } else if (key != null) { + httpParams = httpParams.append(key, value); + } else { + throw Error("key may not be null if value is not object or array"); + } + return httpParams; + } + + /** + * Delete Tool From Project + * @param projectSlug + * @param projectToolId + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public deleteToolFromProject(projectSlug: string, projectToolId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteToolFromProject(projectSlug: string, projectToolId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteToolFromProject(projectSlug: string, projectToolId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteToolFromProject(projectSlug: string, projectToolId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (projectSlug === null || projectSlug === undefined) { + throw new Error('Required parameter projectSlug was null or undefined when calling deleteToolFromProject.'); + } + if (projectToolId === null || projectToolId === undefined) { + throw new Error('Required parameter projectToolId was null or undefined when calling deleteToolFromProject.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/tools/${this.configuration.encodeParam({name: "projectToolId", value: projectToolId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Get Project Tools + * @param projectSlug + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public getProjectTools(projectSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getProjectTools(projectSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getProjectTools(projectSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public getProjectTools(projectSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (projectSlug === null || projectSlug === undefined) { + throw new Error('Required parameter projectSlug was null or undefined when calling getProjectTools.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/tools`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + + /** + * Link Tool To Project + * @param projectSlug + * @param postProjectToolRequest + * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. + * @param reportProgress flag to report request and response progress. + */ + public linkToolToProject(projectSlug: string, postProjectToolRequest: PostProjectToolRequest, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public linkToolToProject(projectSlug: string, postProjectToolRequest: PostProjectToolRequest, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public linkToolToProject(projectSlug: string, postProjectToolRequest: PostProjectToolRequest, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public linkToolToProject(projectSlug: string, postProjectToolRequest: PostProjectToolRequest, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (projectSlug === null || projectSlug === undefined) { + throw new Error('Required parameter projectSlug was null or undefined when calling linkToolToProject.'); + } + if (postProjectToolRequest === null || postProjectToolRequest === undefined) { + throw new Error('Required parameter postProjectToolRequest was null or undefined when calling linkToolToProject.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/tools`; + return this.httpClient.request('post', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + body: postProjectToolRequest, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + +} diff --git a/frontend/src/app/openapi/model/model-provisioning.ts b/frontend/src/app/openapi/model/model-provisioning.ts new file mode 100644 index 0000000000..0e4fde3da9 --- /dev/null +++ b/frontend/src/app/openapi/model/model-provisioning.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { Session } from './session'; + + +export interface ModelProvisioning { + session: Session | null; + provisioned_at: string; + revision: string; + commit_hash: string; +} + diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts index 402c61cc6b..c9d7ced0b9 100644 --- a/frontend/src/app/openapi/model/models.ts +++ b/frontend/src/app/openapi/model/models.ts @@ -69,6 +69,7 @@ export * from './minimal-tool'; export * from './minimal-tool-session-connection-method'; export * from './minimal-tool-version-with-tool'; export * from './model-artifact-status'; +export * from './model-provisioning'; export * from './navbar-configuration-input'; export * from './navbar-configuration-input-external-links-inner'; export * from './navbar-configuration-output'; @@ -97,6 +98,7 @@ export * from './pipeline-run-status'; export * from './post-git-instance'; export * from './post-git-model'; export * from './post-project-request'; +export * from './post-project-tool-request'; export * from './post-project-user'; export * from './post-session-request'; export * from './post-token'; @@ -104,6 +106,7 @@ export * from './post-tool-model'; export * from './post-user'; export * from './project'; export * from './project-status'; +export * from './project-tool'; export * from './project-type'; export * from './project-user'; export * from './project-user-permission'; @@ -139,6 +142,7 @@ export * from './simple-t4-c-model-with-tool-model'; export * from './simple-t4-c-repository'; export * from './simple-t4-c-repository-with-integrations'; export * from './simple-tool-model'; +export * from './simple-tool-model-without-project'; export * from './simple-tool-version'; export * from './status-response'; export * from './submit-t4-c-model'; diff --git a/frontend/src/app/openapi/model/post-project-tool-request.ts b/frontend/src/app/openapi/model/post-project-tool-request.ts new file mode 100644 index 0000000000..b5bad04953 --- /dev/null +++ b/frontend/src/app/openapi/model/post-project-tool-request.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface PostProjectToolRequest { + tool_id: number; + tool_version_id: number; +} + diff --git a/frontend/src/app/openapi/model/post-session-request.ts b/frontend/src/app/openapi/model/post-session-request.ts index 2a02a00281..84eec84afd 100644 --- a/frontend/src/app/openapi/model/post-session-request.ts +++ b/frontend/src/app/openapi/model/post-session-request.ts @@ -17,11 +17,9 @@ export interface PostSessionRequest { tool_id: number; version_id: number; session_type?: SessionType; - /** - * The identifier of the connection method to use - */ - connection_method_id: string; + connection_method_id?: string | null; provisioning?: Array; + project_slug?: string | null; } export namespace PostSessionRequest { } diff --git a/frontend/src/app/openapi/model/project-tool.ts b/frontend/src/app/openapi/model/project-tool.ts new file mode 100644 index 0000000000..639cad051e --- /dev/null +++ b/frontend/src/app/openapi/model/project-tool.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { SimpleToolModelWithoutProject } from './simple-tool-model-without-project'; +import { SimpleToolVersion } from './simple-tool-version'; +import { Tool } from './tool'; + + +export interface ProjectTool { + id: number | null; + tool_version: SimpleToolVersion; + tool: Tool; + used_by: Array; +} + diff --git a/frontend/src/app/openapi/model/session-provisioning-request.ts b/frontend/src/app/openapi/model/session-provisioning-request.ts index 5d3605da58..a02dfa5f6d 100644 --- a/frontend/src/app/openapi/model/session-provisioning-request.ts +++ b/frontend/src/app/openapi/model/session-provisioning-request.ts @@ -15,7 +15,7 @@ export interface SessionProvisioningRequest { project_slug: string; model_slug: string; git_model_id: number; - revision: string; + revision?: string | null; deep_clone: boolean; } diff --git a/frontend/src/app/openapi/model/simple-tool-model-without-project.ts b/frontend/src/app/openapi/model/simple-tool-model-without-project.ts new file mode 100644 index 0000000000..e10293a214 --- /dev/null +++ b/frontend/src/app/openapi/model/simple-tool-model-without-project.ts @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + +import { GitModel } from './git-model'; + + +export interface SimpleToolModelWithoutProject { + id: number; + slug: string; + name: string; + git_models: Array | null; +} + diff --git a/frontend/src/app/openapi/model/tool-model-provisioning-input.ts b/frontend/src/app/openapi/model/tool-model-provisioning-input.ts index 96f7298626..b8a19462da 100644 --- a/frontend/src/app/openapi/model/tool-model-provisioning-input.ts +++ b/frontend/src/app/openapi/model/tool-model-provisioning-input.ts @@ -17,5 +17,9 @@ export interface ToolModelProvisioningInput { */ directory?: string; max_number_of_models?: number | null; + /** + * Specifies if a tool requires provisioning. If enabled and a session without provisioning is requested, it will be declined. + */ + required?: boolean; } diff --git a/frontend/src/app/openapi/model/tool-model-provisioning-output.ts b/frontend/src/app/openapi/model/tool-model-provisioning-output.ts index 968f2ed526..3f52f8babe 100644 --- a/frontend/src/app/openapi/model/tool-model-provisioning-output.ts +++ b/frontend/src/app/openapi/model/tool-model-provisioning-output.ts @@ -17,5 +17,9 @@ export interface ToolModelProvisioningOutput { */ directory: string; max_number_of_models: number | null; + /** + * Specifies if a tool requires provisioning. If enabled and a session without provisioning is requested, it will be declined. + */ + required: boolean; } diff --git a/frontend/src/app/projects/models/create-model/create-model.component.css b/frontend/src/app/projects/models/create-model/create-model.component.css deleted file mode 100644 index 8535c6938a..0000000000 --- a/frontend/src/app/projects/models/create-model/create-model.component.css +++ /dev/null @@ -1,4 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ diff --git a/frontend/src/app/projects/models/create-model/create-model.component.ts b/frontend/src/app/projects/models/create-model/create-model.component.ts index 1716d12e92..cfdc9a9c3e 100644 --- a/frontend/src/app/projects/models/create-model/create-model.component.ts +++ b/frontend/src/app/projects/models/create-model/create-model.component.ts @@ -28,7 +28,6 @@ import { ManageT4CModelComponent } from '../model-source/t4c/manage-t4c-model/ma @Component({ selector: 'app-create-model', templateUrl: './create-model.component.html', - styleUrls: ['./create-model.component.css'], standalone: true, imports: [ MatStepper, diff --git a/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.component.html b/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.component.html index 10a21be6b2..f3b18bd12b 100644 --- a/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.component.html +++ b/frontend/src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-dialog.component.html @@ -95,7 +95,8 @@

View diagrams

} } @else {
error
Diagram export has failed.
diff --git a/frontend/src/app/projects/models/service/model.service.ts b/frontend/src/app/projects/models/service/model.service.ts index a32367563b..c7d3cfe91c 100644 --- a/frontend/src/app/projects/models/service/model.service.ts +++ b/frontend/src/app/projects/models/service/model.service.ts @@ -14,6 +14,7 @@ import { PatchToolModel, PostToolModel, ProjectsModelsService, + SimpleToolModelWithoutProject, ToolModel, } from 'src/app/openapi'; import { GetGitModel } from 'src/app/projects/project-detail/model-overview/model-detail/git-model.service'; @@ -157,6 +158,8 @@ export class ModelWrapperService { } } -export function getPrimaryGitModel(model: ToolModel): GetGitModel | undefined { +export function getPrimaryGitModel( + model: SimpleToolModelWithoutProject, +): GetGitModel | undefined { return model.git_models?.find((gitModel) => gitModel.primary); } diff --git a/frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.html b/frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.html new file mode 100644 index 0000000000..9796609c72 --- /dev/null +++ b/frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.html @@ -0,0 +1,56 @@ + + +
+ @if (projectWrapperService.project$ | async) { +
+
+
+ + Tool + + @for (tool of availableTools$ | async; track tool.id) { + + {{ tool.name }} + + } + + @if (form.controls.tool_id.errors?.min) { + You have to select a tool + } + +
+
+ + Tool Version + + @for (tool of availableToolVersions$ | async; track tool.id) { + + {{ tool.name }} + + } + + @if (form.controls.tool_id.errors?.min) { + You have to select a tool + } + +
+
+ +
+
+
+ } +
diff --git a/frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.ts b/frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.ts new file mode 100644 index 0000000000..712bc97078 --- /dev/null +++ b/frontend/src/app/projects/project-detail/create-project-tools/create-project-tools.component.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CommonModule } from '@angular/common'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; +import { + FormControl, + FormGroup, + ReactiveFormsModule, + Validators, +} from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatIconModule } from '@angular/material/icon'; +import { MatSelectModule } from '@angular/material/select'; +import { ActivatedRoute, Router } from '@angular/router'; +import { BehaviorSubject, filter, switchMap, take } from 'rxjs'; +import { + PostProjectToolRequest, + ProjectsToolsService, + Tool, + ToolsService, + ToolVersion, +} from 'src/app/openapi'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; + +@Component({ + selector: 'app-create-project-tools', + standalone: true, + imports: [ + CommonModule, + MatFormFieldModule, + ReactiveFormsModule, + MatSelectModule, + MatIconModule, + MatButtonModule, + ], + templateUrl: './create-project-tools.component.html', + styles: ` + :host { + display: block; + } + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class CreateProjectToolsComponent implements OnInit { + form = new FormGroup({ + tool_id: new FormControl(-1, [Validators.min(0)]), + tool_version_id: new FormControl(-1, [Validators.min(0)]), + }); + + private readonly availableTools = new BehaviorSubject( + undefined, + ); + availableTools$ = this.availableTools.asObservable(); + + private readonly availableToolVersions = new BehaviorSubject< + ToolVersion[] | undefined + >(undefined); + availableToolVersions$ = this.availableToolVersions.asObservable(); + + constructor( + public projectWrapperService: ProjectWrapperService, + private projectsToolService: ProjectsToolsService, + private toolsService: ToolsService, + private router: Router, + private route: ActivatedRoute, + ) {} + + ngOnInit(): void { + this.loadTools(); + this.form.controls.tool_id.valueChanges.subscribe((value) => { + if (!value) return; + this.loadToolVersions(value); + }); + } + + loadTools() { + this.availableTools.next(undefined); + this.toolsService + .getTools() + .subscribe((tools) => this.availableTools.next(tools)); + } + + loadToolVersions(toolID: number) { + this.availableToolVersions.next(undefined); + this.toolsService + .getToolVersions(toolID) + .subscribe((versions) => this.availableToolVersions.next(versions)); + } + + onSubmit() { + this.projectWrapperService.project$ + .pipe( + take(1), + filter(Boolean), + switchMap((project) => + this.projectsToolService.linkToolToProject( + project.slug, + this.form.value as PostProjectToolRequest, + ), + ), + ) + .subscribe(() => + this.router.navigate(['../..'], { + relativeTo: this.route, + }), + ); + } +} diff --git a/frontend/src/app/projects/project-detail/model-overview/model-complexity-badge/model-complexity-badge.component.html b/frontend/src/app/projects/project-detail/model-overview/model-complexity-badge/model-complexity-badge.component.html index d6d7800ce9..ff229af7d7 100644 --- a/frontend/src/app/projects/project-detail/model-overview/model-complexity-badge/model-complexity-badge.component.html +++ b/frontend/src/app/projects/project-detail/model-overview/model-complexity-badge/model-complexity-badge.component.html @@ -29,7 +29,8 @@ } @else if (errorCode === "FILE_NOT_FOUND") {
open_in_new @@ -40,7 +41,8 @@
} @else {
error -
-
-

Models

- @if (projectUserService.verifyRole("manager")) { - +

Models

+ @if (projectUserService.verifyRole("manager")) { +
+ add +
+
+ @if (models !== undefined && models.length > 1) { + - } + } -
-
- @if ((modelService.models$ | async) === undefined) { - @for (card of [0, 1, 2]; track $index) { - - } + } +
+
+ @if ((modelService.models$ | async) === undefined) { + @for (card of [0, 1, 2]; track $index) { + } - @for (model of modelService.models$ | async; track model.id) { + } + @for (model of modelService.models$ | async; track model.id) { +
-
-
-
- {{ model.name }} -
- - {{ model.tool.name }} - @if (model.version) { - {{ model.version.name }} - } @else { - (Version not specified) - } - +
+
+ {{ model.name }}
+ + {{ model.tool.name }} + @if (model.version) { + {{ model.version.name }} + } @else { + (Version not specified) + } + +
-
-
-
-
Nature
- - @if (model.nature) { - {{ model.nature.name }} - } @else { - Not specified - } - -
+
+
+
+
Nature
+ + @if (model.nature) { + {{ model.nature.name }} + } @else { + Not specified + } +
-
-
-
Working mode
-
- {{ getPrimaryWorkingMode(model) }} -
+
+
+
+
Working mode
+
+ {{ getPrimaryWorkingMode(model) }}
-
-
- {{ model.description || "This model has no description." }} -
-
- @if (model.tool.name === "Capella") { - - } -
-
- @if (userService.user?.role === "administrator") { - - key - - } - @if (projectUserService.verifyRole("manager")) { - - settings - - +
+
+
+ {{ model.description || "This model has no description." }} +
+
+ @if (model.tool.name === "Capella") { + + } +
+
+ @if (userService.user?.role === "administrator") { + + key + + } + @if (projectUserService.verifyRole("manager")) { + + settings + + + + link + + @if (!project?.is_archived && project?.type !== "training") { - link + sync - @if (!project?.is_archived && project?.type !== "training") { - - sync - - } } + } - @if (model.git_models) { - + open_in_new + + @if (model.tool.name === "Capella") { + - } + image_search + } + } - @if ( - !project?.is_archived && - project?.type !== "training" && - model.t4c_models && - projectUserService.verifyPermission("write") - ) { - - screen_share - - } -
+ @if ( + !project?.is_archived && + project?.type !== "training" && + model.t4c_models && + projectUserService.verifyPermission("write") + ) { + + screen_share + + }
- } -
+
+ }
diff --git a/frontend/src/app/projects/project-detail/model-overview/model-overview.component.ts b/frontend/src/app/projects/project-detail/model-overview/model-overview.component.ts index 29e6881024..152afede00 100644 --- a/frontend/src/app/projects/project-detail/model-overview/model-overview.component.ts +++ b/frontend/src/app/projects/project-detail/model-overview/model-overview.component.ts @@ -36,7 +36,6 @@ import { ModelComplexityBadgeComponent } from './model-complexity-badge/model-co @Component({ selector: 'app-model-overview', templateUrl: './model-overview.component.html', - styleUrls: ['./model-overview.component.css'], standalone: true, imports: [ MatAnchor, diff --git a/frontend/src/app/projects/project-detail/project-details.component.html b/frontend/src/app/projects/project-detail/project-details.component.html index 9ff2654a66..8f35e8c939 100644 --- a/frontend/src/app/projects/project-detail/project-details.component.html +++ b/frontend/src/app/projects/project-detail/project-details.component.html @@ -4,12 +4,23 @@ -->
-
+
- + @if ((projectService.project$ | async)?.type !== "training") { + + } +
- + + @if ((projectService.project$ | async)?.type === "training") { + + } @else { + + } +
@if (projectUserService.verifyRole("manager")) { diff --git a/frontend/src/app/projects/project-detail/project-details.component.ts b/frontend/src/app/projects/project-detail/project-details.component.ts index e2c74ac5a6..cc9b6aa0f9 100644 --- a/frontend/src/app/projects/project-detail/project-details.component.ts +++ b/frontend/src/app/projects/project-detail/project-details.component.ts @@ -2,9 +2,14 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { ProjectToolsWrapperService } from 'src/app/projects/project-detail/project-tools/project-tools-wrapper.service'; +import { ProjectToolsComponent } from 'src/app/projects/project-detail/project-tools/project-tools.component'; import { ProjectUserService } from 'src/app/projects/project-detail/project-users/service/project-user.service'; -import { CreateReadonlySessionComponent } from '../../sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component'; +import { TrainingDetailsComponent } from 'src/app/projects/project-detail/training-details/training-details.component'; +import { CreateReadonlySessionComponent } from '../../sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component'; +import { CreateProvisionedSessionComponent } from '../../sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component'; import { ProjectWrapperService } from '../service/project.service'; import { ModelOverviewComponent } from './model-overview/model-overview.component'; import { ProjectMetadataComponent } from './project-metadata/project-metadata.component'; @@ -19,11 +24,20 @@ import { ProjectUserSettingsComponent } from './project-users/project-user-setti CreateReadonlySessionComponent, ModelOverviewComponent, ProjectUserSettingsComponent, + AsyncPipe, + CreateProvisionedSessionComponent, + TrainingDetailsComponent, + ProjectToolsComponent, ], }) -export class ProjectDetailsComponent { +export class ProjectDetailsComponent implements OnInit { constructor( public projectService: ProjectWrapperService, public projectUserService: ProjectUserService, + private projectToolsWrapperService: ProjectToolsWrapperService, ) {} + + ngOnInit(): void { + this.projectToolsWrapperService.loadProjectTools(); + } } diff --git a/frontend/src/app/projects/project-detail/project-details.stories.ts b/frontend/src/app/projects/project-detail/project-details.stories.ts index ec8812c9a4..9ab24e775e 100644 --- a/frontend/src/app/projects/project-detail/project-details.stories.ts +++ b/frontend/src/app/projects/project-detail/project-details.stories.ts @@ -4,6 +4,8 @@ */ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; import { ProjectUserService } from 'src/app/projects/project-detail/project-users/service/project-user.service'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; +import { mockProject, MockProjectWrapperService } from 'src/storybook/project'; import { MockProjectUserService } from 'src/storybook/project-users'; import { ProjectDetailsComponent } from './project-details.component'; @@ -35,3 +37,36 @@ export const LoadingAsProjectLead: Story = { }), ], }; + +export const ProjectLoaded: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: ProjectWrapperService, + useFactory: () => + new MockProjectWrapperService(mockProject, undefined), + }, + ], + }), + ], +}; + +export const TrainingLoaded: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: ProjectWrapperService, + useFactory: () => + new MockProjectWrapperService( + { ...mockProject, type: 'training' }, + undefined, + ), + }, + ], + }), + ], +}; diff --git a/frontend/src/app/projects/project-detail/project-tools/project-tools-wrapper.service.ts b/frontend/src/app/projects/project-detail/project-tools/project-tools-wrapper.service.ts new file mode 100644 index 0000000000..99ec77cb3d --- /dev/null +++ b/frontend/src/app/projects/project-detail/project-tools/project-tools-wrapper.service.ts @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Injectable } from '@angular/core'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { BehaviorSubject, filter, switchMap, take, tap } from 'rxjs'; +import { ProjectsToolsService, ProjectTool } from 'src/app/openapi'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; + +@UntilDestroy() +@Injectable({ + providedIn: 'root', +}) +export class ProjectToolsWrapperService { + constructor( + public projectWrapperService: ProjectWrapperService, + private projectToolService: ProjectsToolsService, + ) {} + + private readonly _projectTools = new BehaviorSubject< + ProjectTool[] | undefined + >(undefined); + readonly projectTools$ = this._projectTools.asObservable(); + + loadProjectTools(): void { + this._projectTools.next(undefined); + this.projectWrapperService.project$ + .pipe( + filter(Boolean), + switchMap((project) => { + return this.projectToolService.getProjectTools(project.slug); + }), + untilDestroyed(this), + take(1), + tap((tools) => { + this._projectTools.next(tools); + }), + ) + .subscribe(); + } +} diff --git a/frontend/src/app/projects/project-detail/project-tools/project-tools.component.html b/frontend/src/app/projects/project-detail/project-tools/project-tools.component.html new file mode 100644 index 0000000000..44f0b1fbeb --- /dev/null +++ b/frontend/src/app/projects/project-detail/project-tools/project-tools.component.html @@ -0,0 +1,104 @@ + + +
+

Used Tools

+ @if (projectUserService.verifyRole("manager")) { + help + + } +
+ +
+ @if ((projectToolsWrapperService.projectTools$ | async) === undefined) { +
+ @for (card of [0, 1]; track card) { + + } +
+ } @else { + + @for ( + tool of projectToolsWrapperService.projectTools$ | async; + track tool.id + ) { + + + + + + } @empty { + The project doesn't use any tools yet. + } + + } +
diff --git a/frontend/src/app/projects/project-detail/project-tools/project-tools.component.ts b/frontend/src/app/projects/project-detail/project-tools/project-tools.component.ts new file mode 100644 index 0000000000..39219fd5c2 --- /dev/null +++ b/frontend/src/app/projects/project-detail/project-tools/project-tools.component.ts @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CommonModule } from '@angular/common'; +import { Component } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { RouterLink } from '@angular/router'; +import { UntilDestroy } from '@ngneat/until-destroy'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { filter, switchMap, take, tap } from 'rxjs'; +import { ToastService } from 'src/app/helpers/toast/toast.service'; +import { ProjectsToolsService, ProjectTool } from 'src/app/openapi'; +import { ProjectToolsWrapperService } from 'src/app/projects/project-detail/project-tools/project-tools-wrapper.service'; +import { ProjectUserService } from 'src/app/projects/project-detail/project-users/service/project-user.service'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; + +@UntilDestroy() +@Component({ + selector: 'app-project-tools', + standalone: true, + imports: [ + CommonModule, + RouterLink, + MatTooltipModule, + MatButtonModule, + MatIconModule, + NgxSkeletonLoaderModule, + ], + templateUrl: './project-tools.component.html', + styles: ` + :host { + display: block; + } + `, +}) +export class ProjectToolsComponent { + constructor( + public projectUserService: ProjectUserService, + public projectWrapperService: ProjectWrapperService, + public projectToolsWrapperService: ProjectToolsWrapperService, + private projectToolService: ProjectsToolsService, + private toastService: ToastService, + ) {} + + unlinkTool(tool: ProjectTool): void { + const tool_id = tool.id; + if (!tool_id) return; + + this.projectWrapperService.project$ + .pipe( + filter(Boolean), + take(1), + switchMap((project) => + this.projectToolService.deleteToolFromProject(project.slug, tool_id), + ), + tap(() => { + this.projectToolsWrapperService.loadProjectTools(); + this.toastService.showSuccess( + 'Tool unlinked from project', + `The tool ${tool.tool.name} ${tool.tool_version.name} was successfully unlinked from the project.`, + ); + }), + ) + .subscribe(); + } +} diff --git a/frontend/src/app/projects/project-detail/project-tools/project-tools.stories.ts b/frontend/src/app/projects/project-detail/project-tools/project-tools.stories.ts new file mode 100644 index 0000000000..c51b1cd5ec --- /dev/null +++ b/frontend/src/app/projects/project-detail/project-tools/project-tools.stories.ts @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { ProjectUserService } from 'src/app/projects/project-detail/project-users/service/project-user.service'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; +import { mockProject, MockProjectWrapperService } from 'src/storybook/project'; +import { + mockProjectTool, + projectToolServiceProvider, +} from 'src/storybook/project-tools'; +import { MockProjectUserService } from 'src/storybook/project-users'; +import { mockCapellaTool, mockCapellaToolVersion } from 'src/storybook/tool'; +import { ProjectToolsComponent } from './project-tools.component'; + +const meta: Meta = { + title: 'Project Components/Used Tools', + component: ProjectToolsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const LoadingAsProjectLead: Story = { + decorators: [ + moduleMetadata({ + providers: [ + { + provide: ProjectUserService, + useValue: new MockProjectUserService('manager', undefined, undefined), + }, + ], + }), + ], +}; + +export const ArchivedProject: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: ProjectWrapperService, + useValue: new MockProjectWrapperService( + { ...mockProject, is_archived: true }, + undefined, + ), + }, + { + provide: ProjectUserService, + useValue: new MockProjectUserService('manager', undefined, undefined), + }, + ], + }), + ], +}; + +const projectTools = [ + mockProjectTool, + { + id: 1, + tool: { ...mockCapellaTool, name: 'Example Tool' }, + tool_version: { ...mockCapellaToolVersion, name: '1.0.0' }, + used_by: [], + }, + { + id: null, + tool: { ...mockCapellaTool, name: 'Tool 3' }, + tool_version: { ...mockCapellaToolVersion, name: 'Latest' }, + used_by: [], + }, +]; + +export const Loaded: Story = { + decorators: [ + moduleMetadata({ + providers: [projectToolServiceProvider(projectTools)], + }), + ], +}; + +export const LoadedAsProjectLead: Story = { + decorators: [ + moduleMetadata({ + providers: [ + projectToolServiceProvider(projectTools), + { + provide: ProjectUserService, + useValue: new MockProjectUserService('manager', undefined, undefined), + }, + ], + }), + ], +}; diff --git a/frontend/src/app/projects/project-detail/training-details/training-details.component.html b/frontend/src/app/projects/project-detail/training-details/training-details.component.html new file mode 100644 index 0000000000..b6ffbbaa66 --- /dev/null +++ b/frontend/src/app/projects/project-detail/training-details/training-details.component.html @@ -0,0 +1,119 @@ + + +
+
+

Training Details

+ @if ( + projectUserService.verifyRole("manager") && + (modelService.models$ | async)?.length === 0 + ) { +
+ add +
+
+ } +
+
+ @if ((modelService.models$ | async) === undefined) { + + } @else { + @for (model of modelService.models$ | async; track model.id) { +
+
+
+ {{ model.tool.name }} ({{ model.version?.name }}) +
+ @if (getPrimaryGitModel(model); as gitModel) { +
+ Revision {{ gitModel.revision }} +
+ } +
+ +
+
+ +
+
+ @if (projectUserService.verifyRole("manager")) { + + settings + + + link + + } + @if (model.git_models) { + + open_in_new + + } +
+
+
+ } @empty { +
+ The training is not configured yet. +
+ } + } +
+
diff --git a/frontend/src/app/projects/project-detail/training-details/training-details.component.ts b/frontend/src/app/projects/project-detail/training-details/training-details.component.ts new file mode 100644 index 0000000000..3a9db11463 --- /dev/null +++ b/frontend/src/app/projects/project-detail/training-details/training-details.component.ts @@ -0,0 +1,92 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { AsyncPipe, CommonModule } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { + MatAnchor, + MatButton, + MatMiniFabAnchor, + MatMiniFabButton, +} from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatTooltip } from '@angular/material/tooltip'; +import { RouterLink } from '@angular/router'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { MarkdownComponent, provideMarkdown } from 'ngx-markdown'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { combineLatest } from 'rxjs'; +import { SKIP_ERROR_HANDLING_CONTEXT } from 'src/app/general/error-handling/error-handling.interceptor'; +import { ProjectsModelsREADMEService, ToolModel } from 'src/app/openapi'; +import { + getPrimaryGitModel, + ModelWrapperService, +} from 'src/app/projects/models/service/model.service'; +import { GetGitModel } from 'src/app/projects/project-detail/model-overview/model-detail/git-model.service'; +import { ProjectUserService } from 'src/app/projects/project-detail/project-users/service/project-user.service'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; + +@UntilDestroy() +@Component({ + selector: 'app-training-details', + standalone: true, + imports: [ + CommonModule, + MatAnchor, + RouterLink, + MatTooltip, + MatIcon, + MatButton, + NgxSkeletonLoaderModule, + MatMiniFabAnchor, + MatMiniFabButton, + AsyncPipe, + MarkdownComponent, + ], + templateUrl: './training-details.component.html', + styles: ` + :host { + display: block; + } + `, + providers: [provideMarkdown()], +}) +export class TrainingDetailsComponent implements OnInit { + constructor( + public modelService: ModelWrapperService, + public projectUserService: ProjectUserService, + public projectService: ProjectWrapperService, + private readmeService: ProjectsModelsREADMEService, + ) {} + + getPrimaryGitModelURL(model: ToolModel): string { + const primaryModel = getPrimaryGitModel(model); + return primaryModel ? primaryModel.path : ''; + } + + getPrimaryGitModel(model: ToolModel): GetGitModel | undefined { + return getPrimaryGitModel(model); + } + + readmes = new Map(); + + ngOnInit(): void { + combineLatest([this.projectService.project$, this.modelService.models$]) + .pipe(untilDestroyed(this)) + .subscribe(([project, models]) => { + if (!models || !project) return; + if (project.type === 'general') return; + for (const model of models) { + this.readmeService + .getReadme(project.slug, model.slug, 'body', false, { + httpHeaderAccept: 'text/markdown', + context: SKIP_ERROR_HANDLING_CONTEXT, + }) + .subscribe((readme) => { + this.readmes.set(model.slug, readme); + }); + } + }); + } +} diff --git a/frontend/src/app/projects/project-detail/training-details/training-details.stories.ts b/frontend/src/app/projects/project-detail/training-details/training-details.stories.ts new file mode 100644 index 0000000000..488b9377a0 --- /dev/null +++ b/frontend/src/app/projects/project-detail/training-details/training-details.stories.ts @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { + mockModel, + mockModelWrapperServiceProvider, +} from 'src/storybook/model'; +import { mockProjectUserServiceProvider } from 'src/storybook/project-users'; +import { TrainingDetailsComponent } from './training-details.component'; + +const meta: Meta = { + title: 'Project Components/Training Details', + component: TrainingDetailsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = {}; + +export const NoModel: Story = { + decorators: [ + moduleMetadata({ + providers: [mockModelWrapperServiceProvider(undefined, [])], + }), + ], +}; + +export const NoModelAsProjectLead: Story = { + decorators: [ + moduleMetadata({ + providers: [ + mockProjectUserServiceProvider('manager'), + mockModelWrapperServiceProvider(undefined, []), + ], + }), + ], +}; + +const readme = ` +# Heading level 1 +## Heading level 2 +### Heading level 3 + +This is an example of **bold text**. +And another example with *italics*. + +- list + - sublist + - test +- item 2 + +1. Ordered list +2. Test + +This is a example text with a [link to an external page](https://example.com). + +You can also use code blocks: + +${'```'}zsh +git clone --recurse-submodules https://github.com/DSD-DBS/capella-collab-manager.git +cd capella-collab-manager +${'```'} +`; + +export const Loaded: Story = { + args: { + readmes: new Map([[mockModel.slug, readme]]), + }, + decorators: [ + moduleMetadata({ + providers: [mockModelWrapperServiceProvider(undefined, [mockModel])], + }), + ], +}; + +export const LoadedAsProjectLead: Story = { + args: { + readmes: new Map([[mockModel.slug, readme]]), + }, + decorators: [ + moduleMetadata({ + providers: [ + mockProjectUserServiceProvider('manager'), + mockModelWrapperServiceProvider(undefined, [mockModel]), + ], + }), + ], +}; diff --git a/frontend/src/app/projects/project-overview/project-overview.component.html b/frontend/src/app/projects/project-overview/project-overview.component.html index f96e999930..23fce40423 100644 --- a/frontend/src/app/projects/project-overview/project-overview.component.html +++ b/frontend/src/app/projects/project-overview/project-overview.component.html @@ -19,14 +19,26 @@
Projects - Trainings + +
+ schoolTrainings +
+
- Internal - Private + +
+ lock_openInternal +
+
+ +
+ lockPrivate +
+
@@ -56,9 +68,14 @@ matRipple class="mat-card-overview collab-card !m-0" > -
+
@if (project.type === "training") { - school + school + } + @if (project.visibility === "private") { + lock + } @else { + lock_open } {{ project.name }} @@ -73,11 +90,8 @@
- {{ - project.visibility === "internal" - ? "Internal project" - : "Private project" - }} + {{ project.visibility === "internal" ? "Internal" : "Private" }} + {{ project.type === "training" ? "training" : "project" }}
{{ project.users.leads }} project admin(s), diff --git a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html index b7c75bde8d..4c833dc2a7 100644 --- a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html +++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.html @@ -21,15 +21,13 @@

[attr.data-testid]="'rating-' + rating" mat-icon-button type="button" - [class]=" - [ - '!flex', - getColorForRating(rating), - this.feedbackForm.get('rating')?.value === rating - ? '!bg-gray-200' - : '', - ].join(' ') - " + [ngClass]="[ + '!flex', + getColorForRating(rating), + this.feedbackForm.get('rating')?.value === rating + ? '!bg-gray-200' + : '', + ]" > @switch (rating) { diff --git a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts index 7748f0815d..68c2d39dba 100644 --- a/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts +++ b/frontend/src/app/sessions/feedback/feedback-dialog/feedback-dialog.component.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { AsyncPipe } from '@angular/common'; +import { AsyncPipe, NgClass } from '@angular/common'; import { Component, Inject } from '@angular/core'; import { FormControl, @@ -55,6 +55,7 @@ interface DialogData { ReactiveFormsModule, FormsModule, AsyncPipe, + NgClass, ], templateUrl: './feedback-dialog.component.html', }) diff --git a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html index a3cab34782..7f954cf29f 100644 --- a/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html +++ b/frontend/src/app/sessions/session/floating-window-manager/floating-window-manager.component.html @@ -18,7 +18,8 @@ (cdkDragEnded)="dragStop()" >
diff --git a/frontend/src/app/sessions/sessions.component.html b/frontend/src/app/sessions/sessions.component.html index 8db643590c..03e77287f1 100644 --- a/frontend/src/app/sessions/sessions.component.html +++ b/frontend/src/app/sessions/sessions.component.html @@ -7,11 +7,8 @@
- - + + +
diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html index c71ea4851f..c9d4c7a8cc 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/active-sessions.component.html @@ -25,7 +25,7 @@

No active sessions

} @else if ((userSessionService.sessions$ | async)?.length !== 0) {
= { ], }), componentWrapperDecorator( - (story) => `
${story}
`, + (story) => `
${story}
`, ), ], }; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/connection-dialog/connection-dialog.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/connection-dialog/connection-dialog.stories.ts index 67feab7a46..3c02705756 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/connection-dialog/connection-dialog.stories.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/active-sessions/connection-dialog/connection-dialog.stories.ts @@ -7,7 +7,7 @@ import { Meta, StoryObj, moduleMetadata } from '@storybook/angular'; import { OwnUserWrapperService } from 'src/app/services/user/user.service'; import { dialogWrapper } from 'src/storybook/decorators'; import { startedSession } from 'src/storybook/session'; -import { mockTool } from 'src/storybook/tool'; +import { mockCapellaTool } from 'src/storybook/tool'; import { MockOwnUserWrapperService, mockUser } from 'src/storybook/user'; import { ConnectionDialogComponent } from './connection-dialog.component'; @@ -47,7 +47,7 @@ export const WithoutTeamForCapella: Story = { ...startedSession, version: { ...startedSession.version, - tool: { ...mockTool, integrations: { t4c: false } }, + tool: { ...mockCapellaTool, integrations: { t4c: false } }, }, }, }, diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.html b/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component.html similarity index 71% rename from frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.html rename to frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component.html index 4b1e0b6269..f7c4f1f142 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.html +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component.html @@ -15,21 +15,26 @@

Read-only Sessions

[formGroup]="toolSelectionForm" (ngSubmit)="requestReadonlySession()" > -
-
- + @if (tools === undefined) { +
+ @for (menu of [0, 1]; track menu) { +
+ +
+ }
-
+ } +
@@ -50,12 +55,11 @@

Read-only Sessions

Version - - {{ version.name }} - + @for (version of this.relevantToolVersions; track version.id) { + + {{ version.name }} + + }
diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component.ts similarity index 93% rename from frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.ts rename to frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component.ts index a9b9b6e51d..bc3d9206dc 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.component.ts @@ -19,7 +19,7 @@ import { MatSelect } from '@angular/material/select'; import { RouterLink } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { combineLatest, filter, Observable, tap } from 'rxjs'; +import { combineLatest, filter, Observable } from 'rxjs'; import { Tool, ToolModel, ToolVersion } from 'src/app/openapi'; import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; import { ProjectWrapperService } from 'src/app/projects/service/project.service'; @@ -28,13 +28,12 @@ import { ToolWrapperService, ToolVersionWithTool, } from 'src/app/settings/core/tools-settings/tool.service'; -import { CreateReadonlySessionDialogComponent } from '../../create-sessions/create-readonly-session/create-readonly-session-dialog.component'; +import { CreateReadonlySessionDialogComponent } from '../create-sessions/create-readonly-session/create-readonly-session-dialog.component'; @UntilDestroy() @Component({ selector: 'app-create-readonly-session', templateUrl: './create-readonly-session.component.html', - styleUrls: ['./create-readonly-session.component.css'], standalone: true, imports: [ FormsModule, @@ -56,7 +55,6 @@ export class CreateReadonlySessionComponent implements OnInit { models?: ModelWithCompatibility[]; relevantToolVersions?: ToolVersion[]; - allToolVersions?: ToolVersionWithTool[]; public toolSelectionForm = this.fb.group({ tool: this.fb.control(null, Validators.required), @@ -91,11 +89,7 @@ export class CreateReadonlySessionComponent implements OnInit { return combineLatest([ this.modelService.models$.pipe(untilDestroyed(this), filter(Boolean)), this.toolWrapperService.getVersionsForTools(), - ]).pipe( - tap(([_, versions]) => { - this.allToolVersions = versions; - }), - ); + ]); } resolveVersionCompatibility( diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.stories.ts new file mode 100644 index 0000000000..07fca679d4 --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-readonly-session/create-readonly-session.stories.ts @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, StoryObj } from '@storybook/angular'; +import { mockModel } from 'src/storybook/model'; +import { mockCapellaToolVersion } from 'src/storybook/tool'; +import { CreateReadonlySessionComponent } from './create-readonly-session.component'; + +const meta: Meta = { + title: 'Session Components/Create Readonly Session', + component: CreateReadonlySessionComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const Loaded: Story = { + args: { + relevantToolVersions: [mockCapellaToolVersion], + models: [{ ...mockModel, compatibleVersions: [] }], + }, +}; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.css b/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.css deleted file mode 100644 index 5c672c4484..0000000000 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-session/create-readonly-session/create-readonly-session.component.css +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -.skeleton-loader { - gap: 10px; -} - -.skeleton-loader-element { - flex-basis: calc(50% - 5px); -} diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.html b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.html index 8d80805df7..b02760b71a 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.html +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.html @@ -24,12 +24,11 @@

Persistent Workspace Session

formControlName="toolId" (selectionChange)="toolSelectionChange($event.value)" > - - {{ tool.name }} - + @for (tool of toolsWithWorkspaceEnabled | async; track tool.id) { + + {{ tool.name }} + + } Please select a valid tool. @@ -37,14 +36,17 @@

Persistent Workspace Session

Version - - {{ version.name }} - (recommended) - (deprecated) - + @for (version of this.versions; track version.id) { + + {{ version.name }} + @if (version.config.is_recommended) { + (recommended) + } + @if (version.config.is_deprecated) { + (deprecated) + } + + }
@@ -84,8 +86,14 @@

Persistent Workspace Session

type="submit" [disabled]="requestInProgress" > - Request a session with a persistent workspace - keyboard_arrow_right + + + Request session + keyboard_arrow_right +
diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts index bd8e7dac97..3cd4dda02e 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.component.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { AsyncPipe, NgFor, NgIf } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, OnInit } from '@angular/core'; import { FormControl, @@ -41,10 +41,8 @@ import { CreateSessionHistoryComponent } from '../create-session-history/create- MatFormField, MatLabel, MatSelect, - NgFor, MatOption, MatError, - NgIf, MatRadioGroup, MatRadioButton, MatButton, @@ -151,7 +149,9 @@ export class CreatePersistentSessionComponent implements OnInit { return this.toolWrapperService.tools$.pipe( map((tools) => tools?.filter( - (tool) => tool.config.persistent_workspaces.mounting_enabled, + (tool) => + tool.config.persistent_workspaces.mounting_enabled && + !tool.config.provisioning.required, ), ), ); diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.stories.ts index 564fc3c7ae..daa94b7360 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.stories.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-persistent-session/create-persistent-session.stories.ts @@ -9,7 +9,7 @@ import { StoryObj, } from '@storybook/angular'; import { BehaviorSubject, Observable } from 'rxjs'; -import { mockTool } from '../../../../../storybook/tool'; +import { mockCapellaTool } from '../../../../../storybook/tool'; import { Tool } from '../../../../openapi'; import { ToolWrapperService } from '../../../../settings/core/tools-settings/tool.service'; import { MockLicenseUsageWrapperService } from '../../../license-indicator/license-indicator.stories'; @@ -22,7 +22,7 @@ const meta: Meta = { decorators: [ componentWrapperDecorator( (story) => - `
+ `
${story}
`, ), @@ -72,7 +72,7 @@ export const Default: Story = { }, { provide: ToolWrapperService, - useFactory: () => new MockToolWrapperService([mockTool]), + useFactory: () => new MockToolWrapperService([mockCapellaTool]), }, ], }), diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.html b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.html new file mode 100644 index 0000000000..c12d6a285e --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.html @@ -0,0 +1,144 @@ + + +
+
+

+ @if ((projectWrapperService.project$ | async)?.type === "training") { + Training Provisioning + } @else { + Session Provisioning + } +

+ experimentExperimental +
+ +
+
+

+ Provision sessions and a workspace for this + {{ projectDisplayName$ | async }}. The + {{ projectDisplayName$ | async }} will be provisioned once and is + persistent from then on. You'll be able to reset the workspace to the + original state at any time. + @if ((projectWrapperService.project$ | async)?.type === "general") { + Please note that TeamForCapella repositories are not supported for + automatic provisioning yet. + } +

+ +
+
+ @if (provisioningPerTool?.length) { + A provisioning will spawn the following sessions: + @for (tool of provisioningPerTool; track $index) { +
+ {{ tool.tool.name }} {{ tool.tool_version.name }} + + @for (model of tool.used_by; track model.slug) { +
+ play_circle +
+ {{ model.name }} + @if (model.provisioning; as provisioning) { + was provisioned at + {{ provisioning.provisioned_at | date }} with the + revision {{ provisioning.revision }} ({{ + provisioning.commit_hash + }}). + } @else { + will be provisioned. + } +
+
+ } @empty { +
+ cancel +
The session will be created, but not provisioned.
+
+ } +
+ } + } @else if (provisioningPerTool === undefined) { +
+ +
+ } @else { + Please add at least one tool to the project to provision a session. + } +
+
+ +
+
+ + @if (provisioningPerTool?.length) { + + } @else if (provisioningPerTool === undefined) { +
+ +
+ } +
+
+
diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.stories.ts new file mode 100644 index 0000000000..2626f07dea --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.stories.ts @@ -0,0 +1,131 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { mockSimpleToolModelWithoutProject } from 'src/storybook/model'; +import { + mockProject, + mockProjectWrapperServiceProvider, +} from 'src/storybook/project'; +import { mockProjectTool, mockTrainingTool } from 'src/storybook/project-tools'; +import { CreateProvisionedSessionComponent } from './create-provisioned-session.component'; + +const meta: Meta = { + title: 'Session Components/Create Provisioned Session', + component: CreateProvisionedSessionComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const LoadedProjectWithoutTools: Story = { + args: { + provisioningPerTool: [], + }, + decorators: [ + moduleMetadata({ + providers: [mockProjectWrapperServiceProvider(mockProject)], + }), + ], +}; + +export const LoadedProjectWithoutModels: Story = { + args: { + provisioningPerTool: [ + { + ...mockProjectTool, + used_by: [], + }, + ], + }, + decorators: [ + moduleMetadata({ + providers: [mockProjectWrapperServiceProvider(mockProject)], + }), + ], +}; + +const toolProvisioning = { + ...mockSimpleToolModelWithoutProject, + provisioning: null, +}; + +const coffeeMachineProvisioning = { + id: 2, + slug: 'coffee-machine', + name: 'Coffee machine', + git_models: [], + provisioning: { + session: null, + provisioned_at: '2024-04-29T14:00:00Z', + revision: 'main', + commit_hash: 'db45166576e7f1e7fec3256e8657ba431f9b5b77', + }, +}; + +export const LoadedProject: Story = { + args: { + provisioningPerTool: [ + { + ...mockProjectTool, + used_by: [toolProvisioning, coffeeMachineProvisioning], + }, + ], + }, + decorators: [ + moduleMetadata({ + providers: [mockProjectWrapperServiceProvider(mockProject)], + }), + ], +}; + +export const LoadedProjectContinue: Story = { + args: { + provisioningPerTool: [ + { + ...mockProjectTool, + used_by: [coffeeMachineProvisioning], + }, + ], + }, + decorators: [ + moduleMetadata({ + providers: [mockProjectWrapperServiceProvider(mockProject)], + }), + ], +}; + +export const LoadedTraining: Story = { + args: { + provisioningPerTool: [ + { + ...mockProjectTool, + used_by: [], + }, + { + ...mockTrainingTool, + used_by: [ + { + id: 3, + slug: 'pvmt-training', + name: 'PVMT Training', + git_models: [], + provisioning: null, + }, + ], + }, + ], + }, + decorators: [ + moduleMetadata({ + providers: [ + mockProjectWrapperServiceProvider({ ...mockProject, type: 'training' }), + ], + }), + ], +}; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.ts new file mode 100644 index 0000000000..ead4d2cb7c --- /dev/null +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-provisioned-session/create-provisioned-session.component.ts @@ -0,0 +1,224 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { CommonModule, KeyValuePipe } from '@angular/common'; +import { Component, OnInit } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDialog } from '@angular/material/dialog'; +import { MatIconModule } from '@angular/material/icon'; +import { Router } from '@angular/router'; +import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; +import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { combineLatest, map, Observable, of, switchMap, take, tap } from 'rxjs'; +import { ConfirmationDialogComponent } from 'src/app/helpers/confirmation-dialog/confirmation-dialog.component'; +import { MatIconComponent } from 'src/app/helpers/mat-icon/mat-icon.component'; +import { ToastService } from 'src/app/helpers/toast/toast.service'; +import { + ModelProvisioning, + ProjectsModelsProvisioningService, + ProjectTool, + ProjectType, + SessionProvisioningRequest, + SessionsService, + SessionType, + SimpleToolModelWithoutProject, +} from 'src/app/openapi'; +import { getPrimaryGitModel } from 'src/app/projects/models/service/model.service'; +import { ProjectToolsWrapperService } from 'src/app/projects/project-detail/project-tools/project-tools-wrapper.service'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; +import { SessionService } from 'src/app/sessions/service/session.service'; + +@UntilDestroy() +@Component({ + selector: 'app-create-provisioned-session', + standalone: true, + imports: [ + CommonModule, + MatIconModule, + MatButtonModule, + NgxSkeletonLoaderModule, + MatIconComponent, + KeyValuePipe, + ], + templateUrl: './create-provisioned-session.component.html', + styles: ` + :host { + display: block; + } + `, +}) +export class CreateProvisionedSessionComponent implements OnInit { + constructor( + public sessionService: SessionService, + public projectWrapperService: ProjectWrapperService, + private provisioningService: ProjectsModelsProvisioningService, + private projectToolsWrapperService: ProjectToolsWrapperService, + private sessionsService: SessionsService, + private toastService: ToastService, + private router: Router, + private dialog: MatDialog, + ) {} + + provisioningRequestInProgress = false; + + provisioningPerTool: ProjectToolWithProvisioning[] | undefined = undefined; + get provisioningRequired(): boolean { + return ( + this.provisioningPerTool?.some((tool) => + tool.used_by.some((model) => !model.provisioning), + ) ?? true + ); + } + + ngOnInit(): void { + this.loadProvisioningInfo().subscribe(); + } + + get projectDisplayName$(): Observable { + return this.projectWrapperService.project$.pipe( + map((project) => { + if (project?.type === ProjectType.Training) { + return 'training'; + } else { + return 'project'; + } + }), + ); + } + + loadProvisioningInfo() { + return combineLatest([ + this.projectWrapperService.project$, + this.projectToolsWrapperService.projectTools$, + ]).pipe( + tap(([_, tools]) => { + this.provisioningPerTool = tools as ProjectToolWithProvisioning[]; + }), + switchMap(([project, tools]) => { + if (!tools || !project) { + return of(undefined); + } + return combineLatest( + tools.map((tool) => + combineLatest( + tool.used_by.map((model) => + this.provisioningService + .getProvisioning(project.slug, model.slug) + .pipe( + tap((provisioning) => { + const foundModel = this.provisioningPerTool + ?.find( + (provisioningTool) => provisioningTool.id === tool.id, + ) + ?.used_by?.find( + (provisioningModel) => + provisioningModel.slug === model.slug, + ); + + if (foundModel) { + foundModel.provisioning = provisioning; + } + }), + ), + ), + ), + ), + ); + }), + untilDestroyed(this), + ); + } + + provisionWorkspace(): void { + this.projectWrapperService.project$.pipe(take(1)).subscribe((project) => { + this.provisioningRequestInProgress = true; + if (!this.provisioningPerTool || !project) return; + const requests = []; + for (const tool of this.provisioningPerTool) { + const provisioningRequests: SessionProvisioningRequest[] = []; + for (const model of tool.used_by) { + const primaryGitModel = getPrimaryGitModel(model); + if (!primaryGitModel) { + this.toastService.showError( + `Couldn't provision ${model.name}`, + `It has no linked Git repository`, + ); + continue; + } + provisioningRequests.push({ + project_slug: project.slug, + model_slug: model.slug, + git_model_id: primaryGitModel.id, + deep_clone: true, + }); + } + + requests.push( + this.sessionsService.requestSession({ + tool_id: tool.tool.id, + version_id: tool.tool_version.id, + session_type: SessionType.Persistent, + provisioning: provisioningRequests, + project_slug: project.slug, + }), + ); + } + combineLatest(requests).subscribe({ + next: () => { + this.provisioningRequestInProgress = false; + this.router.navigateByUrl('/'); + }, + error: () => { + this.provisioningRequestInProgress = false; + }, + }); + }); + } + + resetProvisioning() { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + title: 'Reset provisioning', + text: + 'Do you really want to reset the provisioning information?' + + ' This will reset the existing provisioned workspace to the latest state during the next session start.' + + ' You will lose all changes made in the workspace.', + }, + }); + dialogRef.afterClosed().subscribe((result) => { + if (!result) return; + this.projectWrapperService.project$.pipe(take(1)).subscribe((project) => { + if (!this.provisioningPerTool || !project) return; + combineLatest( + this.provisioningPerTool + .map((tool) => tool.used_by) + .flat() + .map((model) => { + if (model.provisioning) { + return this.provisioningService.resetProvisioning( + project.slug, + model.slug, + ); + } + return of(undefined); + }), + ).subscribe(() => { + this.toastService.showSuccess( + 'Provisioning reset successful', + 'The provisioning information has been cleared successfully. You can now start a new provisioning to fetch the latest data.', + ); + this.loadProvisioningInfo().pipe(take(1)).subscribe(); + }); + }); + }); + } +} + +type ModelWithProvisioning = { + provisioning: ModelProvisioning | undefined | null; +} & SimpleToolModelWithoutProject; + +type ProjectToolWithProvisioning = Omit & { + used_by: ModelWithProvisioning[]; +}; diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts index e408602d72..742bfe52a8 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-readonly-session/create-readonly-session-dialog.stories.ts @@ -9,7 +9,7 @@ import { SessionService } from 'src/app/sessions/service/session.service'; import { dialogWrapper } from 'src/storybook/decorators'; import { mockPrimaryGitModel } from 'src/storybook/git'; import { createModelWithId } from 'src/storybook/model'; -import { mockTool, mockToolVersion } from 'src/storybook/tool'; +import { mockCapellaTool, mockCapellaToolVersion } from 'src/storybook/tool'; import { CreateReadonlySessionDialogComponent } from './create-readonly-session-dialog.component'; class MockSessionService implements Partial {} @@ -25,12 +25,12 @@ const meta: Meta = { ], }; -const tool: Tool = { ...mockTool }; +const tool: Tool = { ...mockCapellaTool }; tool.config.provisioning.max_number_of_models = 1; const data = { tool: tool, - toolVersion: mockToolVersion, + toolVersion: mockCapellaToolVersion, models: [], projectSlug: '', }; @@ -123,7 +123,11 @@ export const ShowNoteForCompatibleSession: Story = { ], data: { tool: { ...tool, id: 2, name: 'compatibleTool' }, - toolVersion: { ...mockToolVersion, id: 2, name: 'compatibleVersion' }, + toolVersion: { + ...mockCapellaToolVersion, + id: 2, + name: 'compatibleVersion', + }, models: [], projectSlug: '', }, diff --git a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.html b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.html index 4f4e1d6373..85011069ed 100644 --- a/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.html +++ b/frontend/src/app/sessions/user-sessions-wrapper/create-sessions/create-session-history/create-session-history.component.html @@ -17,7 +17,8 @@ } @for (session of sortedResolvedHistory; track $index) {