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