From 100d7a3a64683c65baa4e94870f77e594deb41f8 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 | 2 + .../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/injectables.py | 26 + .../toolmodels/provisioning/models.py | 66 + .../toolmodels/provisioning/routes.py | 44 + .../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 + .../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/provisioning.py | 249 +- .../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/models.py | 33 +- .../capellacollab/sessions/operators/k8s.py | 7 +- backend/capellacollab/sessions/routes.py | 88 +- 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 + .../projects/toolmodels/test_provisioning.py | 10 + backend/tests/sessions/hooks/conftest.py | 89 + .../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_provisioning_hook.py | 171 +- .../sessions/hooks/test_pure_variants.py | 22 +- .../hooks/test_session_preparation.py | 21 +- backend/tests/sessions/hooks/test_t4c_hook.py | 96 +- backend/tests/sessions/test_session_hooks.py | 54 +- backend/tests/sessions/test_session_routes.py | 4 + backend/tests/settings/test_git_instances.py | 24 +- docs/docs/admin/tools/configuration.md | 7 + 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 + ...t-workspace-session-configuration-input.ts | 4 + ...-workspace-session-configuration-output.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 + .../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 | 10 +- .../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 | 95 + .../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 +- 138 files changed, 6271 insertions(+), 1367 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/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/tests/projects/toolmodels/test_provisioning.py create mode 100644 backend/tests/sessions/hooks/conftest.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 60be1b7795..1a7cc2f7b5 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -85,7 +85,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 03ffbc9ec4..c2280f3de9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -100,6 +100,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/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..975200dd9a --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/provisioning/routes.py @@ -0,0 +1,44 @@ +# 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.users import models as projects_users_models + +from . import crud, 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 = fastapi.Depends( + injectables.get_model_provisioning + ), + 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. + """ + + 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/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/provisioning.py b/backend/capellacollab/sessions/hooks/provisioning.py index 4d1b92c902..e5803d9b12 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,158 @@ 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.persistent_workspaces.requires_provisioning: + 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 not existing_provisioning: + 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 + ) + environment["WORKSPACE_DIR"] = str( + pathlib.PurePosixPath("/workspace") + / request.project_scope.slug + / ("tool-" + str(request.tool_version.tool_id)) + ) + + @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 +232,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 +267,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 +299,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 +320,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 +337,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 +348,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/models.py b/backend/capellacollab/sessions/models.py index 566811145e..7fea964ad7 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 if no provisioning information is provided." + " 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..aa9e4606e7 100644 --- a/backend/capellacollab/sessions/routes.py +++ b/backend/capellacollab/sessions/routes.py @@ -17,8 +17,10 @@ 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.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 +84,7 @@ ], ), ) -def request_session( +async def request_session( body: models.PostSessionRequest, user: users_models.DatabaseUser = fastapi.Depends( users_injectables.get_own_user @@ -95,27 +97,28 @@ 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: + 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 +128,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 +228,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 +373,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..06e0c832ff 100644 --- a/backend/capellacollab/tools/models.py +++ b/backend/capellacollab/tools/models.py @@ -280,6 +280,13 @@ class PersistentWorkspaceSessionConfiguration(core_pydantic.BaseModel): "If disabled, persistent workspace sessions can no longer be requested." ), ) + requires_provisioning: 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 ToolSessionConfiguration(core_pydantic.BaseModel): diff --git a/backend/pyproject.toml b/backend/pyproject.toml index d5ab33f627..f4f14a75af 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -71,6 +71,7 @@ dev = [ "aioresponses", "types-lxml", "types-croniter", + "pytest-asyncio", ] [tool.black] diff --git a/backend/tests/projects/toolmodels/test_provisioning.py b/backend/tests/projects/toolmodels/test_provisioning.py new file mode 100644 index 0000000000..2f6b846bd7 --- /dev/null +++ b/backend/tests/projects/toolmodels/test_provisioning.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +def test_get_provisioning(): + raise NotImplementedError() + + +def test_delete_provisioning(): + raise NotImplementedError() diff --git a/backend/tests/sessions/hooks/conftest.py b/backend/tests/sessions/hooks/conftest.py new file mode 100644 index 0000000000..7a0f9b4683 --- /dev/null +++ b/backend/tests/sessions/hooks/conftest.py @@ -0,0 +1,89 @@ +# 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.projects import models as projects_models +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, + project: projects_models.DatabaseProject, +) -> 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=project, + ) + + +@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_provisioning_hook.py b/backend/tests/sessions/hooks/test_provisioning_hook.py index fe83c0c2cd..33049c4488 100644 --- a/backend/tests/sessions/hooks/test_provisioning_hook.py +++ b/backend/tests/sessions/hooks/test_provisioning_hook.py @@ -12,37 +12,35 @@ ) 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.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_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 +65,35 @@ 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, +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.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 +105,49 @@ 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, +async def test_tool_model_mismatch( 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 provided model doesn't match the selected tool""" + 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( +async def test_provision_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 +155,33 @@ 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.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 = admin + response = await hooks_provisioning.ProvisionWorkspaceHook().async_configuration_hook( + configuration_hook_request ) assert response["environment"]["CAPELLACOLLAB_SESSION_PROVISIONING"] + + +async def test_persistent_provisioning(): + raise NotImplementedError() + + +async def test_persistent_provisioning_init(): + raise NotImplementedError() + + +async def test_persistent_provisioning_too_many_models(): + raise NotImplementedError() + + +async def test_fallback_without_revision(): + raise NotImplementedError() 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_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..bb930703fd 100644 --- a/backend/tests/sessions/test_session_routes.py +++ b/backend/tests/sessions/test_session_routes.py @@ -246,3 +246,7 @@ 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] + + +def test_request_session_wo_connection_method(): + raise NotImplementedError() diff --git a/backend/tests/settings/test_git_instances.py b/backend/tests/settings/test_git_instances.py index f28fd0451b..8fa612ee53 100644 --- a/backend/tests/settings/test_git_instances.py +++ b/backend/tests/settings/test_git_instances.py @@ -7,7 +7,7 @@ from fastapi import testclient from sqlalchemy import orm -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 crud as git_crud from capellacollab.settings.modelsources.git import models as git_models @@ -104,15 +104,15 @@ 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", - ] + 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): @@ -120,7 +120,9 @@ def mock_ls_remote(*args, **kwargs): f.set_result(ls_remote) return f - monkeypatch.setattr(git_core, "ls_remote", mock_ls_remote) + monkeypatch.setattr( + instances_git_core, "_ls_remote_command", mock_ls_remote + ) response = client.post( "/api/v1/settings/modelsources/git/revisions", 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/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/persistent-workspace-session-configuration-input.ts b/frontend/src/app/openapi/model/persistent-workspace-session-configuration-input.ts index ac926f282e..247f986bd6 100644 --- a/frontend/src/app/openapi/model/persistent-workspace-session-configuration-input.ts +++ b/frontend/src/app/openapi/model/persistent-workspace-session-configuration-input.ts @@ -16,5 +16,9 @@ export interface PersistentWorkspaceSessionConfigurationInput { * Enables workspace mounting to persistent workspace sessions of this tool. If disabled, persistent workspace sessions can no longer be requested. */ mounting_enabled?: boolean; + /** + * Specifies if a tool requires provisioning. If enabled and a session without provisioning is requested, it will be declined. + */ + requires_provisioning?: boolean; } diff --git a/frontend/src/app/openapi/model/persistent-workspace-session-configuration-output.ts b/frontend/src/app/openapi/model/persistent-workspace-session-configuration-output.ts index 724a131599..7d7bed6f7c 100644 --- a/frontend/src/app/openapi/model/persistent-workspace-session-configuration-output.ts +++ b/frontend/src/app/openapi/model/persistent-workspace-session-configuration-output.ts @@ -16,5 +16,9 @@ export interface PersistentWorkspaceSessionConfigurationOutput { * Enables workspace mounting to persistent workspace sessions of this tool. If disabled, persistent workspace sessions can no longer be requested. */ mounting_enabled: boolean; + /** + * Specifies if a tool requires provisioning. If enabled and a session without provisioning is requested, it will be declined. + */ + requires_provisioning: boolean; } 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/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..49275167ea 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,13 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ +import { AsyncPipe } from '@angular/common'; import { Component } from '@angular/core'; +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,6 +23,10 @@ import { ProjectUserSettingsComponent } from './project-users/project-user-setti CreateReadonlySessionComponent, ModelOverviewComponent, ProjectUserSettingsComponent, + AsyncPipe, + CreateProvisionedSessionComponent, + TrainingDetailsComponent, + ProjectToolsComponent, ], }) export class ProjectDetailsComponent { 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..56c695220c --- /dev/null +++ b/frontend/src/app/projects/project-detail/training-details/training-details.component.ts @@ -0,0 +1,95 @@ +/* + * 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 { 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-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, + private projectToolsWrapperService: ProjectToolsWrapperService, + ) {} + + 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 { + this.projectToolsWrapperService.loadProjectTools(); + 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..2d1926f1c8 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.persistent_workspaces.requires_provisioning, ), ), ); 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) {