From 0c963652997b1827258683b1e612b217fe72b6d3 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Wed, 31 Jan 2024 17:23:34 +0100 Subject: [PATCH] feat: Set tool configuration with YAML --- ...ac7_migrate_tools_to_json_configuration.py | 48 +++++ .../capellacollab/core/database/__init__.py | 1 + .../capellacollab/core/database/decorator.py | 64 ++++++ .../capellacollab/core/database/migration.py | 49 +++-- backend/capellacollab/core/database/models.py | 1 - backend/capellacollab/core/exceptions.py | 3 + .../toolmodels/restrictions/routes.py | 5 +- .../modelsources/t4c/repositories/models.py | 2 +- backend/capellacollab/tools/crud.py | 69 +++---- .../tools/integrations/__init__.py | 2 - .../capellacollab/tools/integrations/crud.py | 21 -- .../tools/integrations/models.py | 44 ---- .../tools/integrations/routes.py | 34 --- backend/capellacollab/tools/models.py | 194 +++++++++++++----- backend/capellacollab/tools/routes.py | 126 ++++++------ docs/docs/admin/settings/tools/index.md | 42 +++- frontend/angular.json | 20 +- frontend/package-lock.json | 115 ++++++++++- frontend/package.json | 14 +- frontend/src/app/app-routing.module.ts | 3 +- frontend/src/app/app.module.ts | 2 + .../app/helpers/editor/editor.component.html | 6 +- .../app/helpers/editor/editor.component.ts | 51 +++-- .../configuration-settings.component.html | 2 +- .../configuration-settings.component.ts | 1 - .../create-tool/create-tool.component.html | 22 ++ .../create-tool/create-tool.component.ts | 40 ++++ .../tool-deletion-dialog.component.html | 8 +- .../tool-details/tool-details.component.css | 5 - .../tool-details/tool-details.component.html | 140 ++----------- .../tool-details/tool-details.component.ts | 190 +++-------------- .../tool-nature/tool-nature.component.html | 111 +++++----- .../tool-nature/tool-nature.component.ts | 129 +++++++----- .../tool-version/tool-version.component.css | 13 +- .../tool-version/tool-version.component.html | 133 ++++++------ .../tool-version/tool-version.component.ts | 150 ++++++-------- .../core/tools-settings/tool.service.ts | 107 +++++----- frontend/tailwind.config.js | 5 + frontend/webpack.config.ts | 16 +- 39 files changed, 1067 insertions(+), 921 deletions(-) create mode 100644 backend/capellacollab/alembic/versions/c973be2e2ac7_migrate_tools_to_json_configuration.py create mode 100644 backend/capellacollab/core/database/decorator.py delete mode 100644 backend/capellacollab/tools/integrations/__init__.py delete mode 100644 backend/capellacollab/tools/integrations/crud.py delete mode 100644 backend/capellacollab/tools/integrations/models.py delete mode 100644 backend/capellacollab/tools/integrations/routes.py create mode 100644 frontend/src/app/settings/core/tools-settings/create-tool/create-tool.component.html create mode 100644 frontend/src/app/settings/core/tools-settings/create-tool/create-tool.component.ts diff --git a/backend/capellacollab/alembic/versions/c973be2e2ac7_migrate_tools_to_json_configuration.py b/backend/capellacollab/alembic/versions/c973be2e2ac7_migrate_tools_to_json_configuration.py new file mode 100644 index 0000000000..ce97f44559 --- /dev/null +++ b/backend/capellacollab/alembic/versions/c973be2e2ac7_migrate_tools_to_json_configuration.py @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +"""Migrate tools to JSON configuration + +Revision ID: c973be2e2ac7 +Revises: 86ab7d4d1684 +Create Date: 2024-01-31 17:40:31.743565 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "c973be2e2ac7" +down_revision = "86ab7d4d1684" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("tool_integrations") + op.add_column( + "tools", + sa.Column( + "integrations", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + server_default=sa.text("'{}'::jsonb"), + ), + ) + op.drop_column("tools", "docker_image_backup_template") + op.drop_column("tools", "docker_image_template") + op.drop_column("tools", "readonly_docker_image_template") + op.add_column( + "versions", + sa.Column( + "config", + postgresql.JSONB(astext_type=sa.Text()), + nullable=False, + server_default=sa.text("'{}'::jsonb"), + ), + ) + op.drop_column("versions", "is_deprecated") + op.drop_column("versions", "is_recommended") + # ### end Alembic commands ### diff --git a/backend/capellacollab/core/database/__init__.py b/backend/capellacollab/core/database/__init__.py index 12fdc4be6d..a742b1761a 100644 --- a/backend/capellacollab/core/database/__init__.py +++ b/backend/capellacollab/core/database/__init__.py @@ -21,6 +21,7 @@ class Base(orm.DeclarativeBase): type_annotation_map = { dict[str, str]: postgresql.JSONB, dict[str, t.Any]: postgresql.JSONB, + dict[str, bool]: postgresql.JSONB, } diff --git a/backend/capellacollab/core/database/decorator.py b/backend/capellacollab/core/database/decorator.py new file mode 100644 index 0000000000..c11aeae930 --- /dev/null +++ b/backend/capellacollab/core/database/decorator.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 +from sqlalchemy import types +from sqlalchemy.dialects import postgresql + + +class PydanticDecorator(types.TypeDecorator): + """Maps a pydantic object to a JSONB column and vice versa. + + Use in Database models like this: + ```py + json_column: orm.Mapped[pydantic.BaseModel] = orm.mapped_column(PydanticDecorator(pydantic.BaseModel)) + ``` + + Replace: + - `json_column` with the name of the column in the database + - `pydantic.BaseModel` with the pydantic model you want to use + """ + + impl = postgresql.JSONB + python_type = pydantic.BaseModel + + cache_ok = True + + def __init__(self, pydantic_model: t.Type[pydantic.BaseModel]): + super().__init__() + self.pydantic_model = pydantic_model + + def process_bind_param(self, value, dialect): + """Convert a pydantic object to JSONB.""" + if value is None: + return None + return value.model_dump() + + def process_literal_param(self, value, dialect): + """Convert a literal pydantic object to JSONB.""" + if value is None: + return None + return value.model_dump() + + def process_result_value(self, value, dialect): + """Convert JSONB to a pydantic object.""" + if value is None: + return None + return self.pydantic_model.model_validate(value) + + +class PydanticDatabaseModel(pydantic.BaseModel): + """Base class for database models with an ID. + + Use it to extend pydantic models with the database ID field: + ```py + class PydanticModel(PydanticSuperModel, decorator.PydanticDatabaseModel): + pass + ``` + """ + + id: int = pydantic.Field( + description="Unique identifier of the resource.", ge=1 + ) diff --git a/backend/capellacollab/core/database/migration.py b/backend/capellacollab/core/database/migration.py index 66ddbc1491..b6e9342778 100644 --- a/backend/capellacollab/core/database/migration.py +++ b/backend/capellacollab/core/database/migration.py @@ -36,8 +36,6 @@ ) from capellacollab.tools import crud as tools_crud from capellacollab.tools import models as tools_models -from capellacollab.tools.integrations import crud as integrations_crud -from capellacollab.tools.integrations import models as integrations_models from capellacollab.users import crud as users_crud from capellacollab.users import models as users_models @@ -120,18 +118,48 @@ def create_tools(db): if os.getenv("DEVELOPMENT_MODE", "").lower() in ("1", "true", "t"): capella = tools_models.DatabaseTool( name="Capella", - docker_image_template=f"{registry}/capella/remote:$version-latest", - docker_image_backup_template=f"{registry}/t4c/client/base:$version-latest", - readonly_docker_image_template=f"{registry}/capella/readonly:$version-latest", + # docker_image_template=f"{registry}/capella/remote:$version-latest", + # docker_image_backup_template=f"{registry}/t4c/client/base:$version-latest", + # readonly_docker_image_template=f"{registry}/capella/readonly:$version-latest", ) + for capella_version in ("5.0.0", "5.2.0", "6.0.0", "6.1.0"): + capella_database_version = tools_models.DatabaseVersion( + name=capella_version, + config=tools_models.ToolVersionConfiguration( + is_recommended=False, + is_deprecated=False, + ), + tool=capella, + ) + tools_crud.create_version( + db, + capella.id, + capella_database_version, + ) + papyrus = tools_models.DatabaseTool( name="Papyrus", - docker_image_template=f"{registry}/papyrus/client/remote:$version-prod", + integrations=tools_models.ToolIntegrations( + t4c=False, pure_variants=False, jupyter=False + ), ) tools_crud.create_tool(db, papyrus) - tools_crud.create_version(db, papyrus.id, "6.1") + for papyrus_version in ("6.0", "6.1"): + papyrus_database_version = tools_models.DatabaseVersion( + name=papyrus_version, + config=tools_models.ToolVersionConfiguration( + is_recommended=False, + is_deprecated=False, + ), + tool=papyrus, + ) + + tools_crud.create_version( + db, + papyrus.id, + ) tools_crud.create_version(db, papyrus.id, "6.0") tools_crud.create_nature(db, papyrus.id, "UML 2.5") @@ -151,14 +179,9 @@ def create_tools(db): jupyter = tools_models.DatabaseTool( name="Jupyter", - docker_image_template=f"{registry}/jupyter-notebook:$version", + integrations=tools_models.ToolIntegrations(jupyter=True), ) tools_crud.create_tool(db, jupyter) - integrations_crud.update_integrations( - db, - jupyter.integrations, - integrations_models.PatchToolIntegrations(jupyter=True), - ) default_version = tools_crud.create_version(db, capella.id, "6.0.0", True) tools_crud.create_version(db, capella.id, "5.2.0") diff --git a/backend/capellacollab/core/database/models.py b/backend/capellacollab/core/database/models.py index 84bd3650c3..6ad2c6a72a 100644 --- a/backend/capellacollab/core/database/models.py +++ b/backend/capellacollab/core/database/models.py @@ -19,7 +19,6 @@ import capellacollab.settings.integrations.purevariants.models import capellacollab.settings.modelsources.git.models import capellacollab.settings.modelsources.t4c.models -import capellacollab.tools.integrations.models import capellacollab.tools.models import capellacollab.users.models import capellacollab.users.tokens.models diff --git a/backend/capellacollab/core/exceptions.py b/backend/capellacollab/core/exceptions.py index a50c0ff49f..787c97bf3c 100644 --- a/backend/capellacollab/core/exceptions.py +++ b/backend/capellacollab/core/exceptions.py @@ -56,3 +56,6 @@ def register_exceptions(app: fastapi.FastAPI): app.add_exception_handler( ExistingDependenciesError, existing_dependencies_exception_handler # type: ignore[arg-type] ) + app.add_exception_handler( + ResourceAlreadyExistsError, resource_already_exists_exception_handler # type: ignore[arg-type] + ) diff --git a/backend/capellacollab/projects/toolmodels/restrictions/routes.py b/backend/capellacollab/projects/toolmodels/restrictions/routes.py index 9819b93274..60984dba4f 100644 --- a/backend/capellacollab/projects/toolmodels/restrictions/routes.py +++ b/backend/capellacollab/projects/toolmodels/restrictions/routes.py @@ -46,7 +46,10 @@ def update_restrictions( ), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseToolModelRestrictions: - if body.allow_pure_variants and not model.tool.integrations.pure_variants: + if ( + body.allow_pure_variants + and not model.tool.integrations["pure_variants"] + ): raise fastapi.HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail={ diff --git a/backend/capellacollab/settings/modelsources/t4c/repositories/models.py b/backend/capellacollab/settings/modelsources/t4c/repositories/models.py index 2786d71f15..061355f083 100644 --- a/backend/capellacollab/settings/modelsources/t4c/repositories/models.py +++ b/backend/capellacollab/settings/modelsources/t4c/repositories/models.py @@ -45,7 +45,7 @@ class DatabaseT4CRepository(database.Base): class CreateT4CRepository(pydantic.BaseModel): name: str = pydantic.Field( - pattern="^[-a-zA-Z0-9_]+$", examples=["testrepo"] + pattern=r"^[-a-zA-Z0-9_]+$", examples=["testrepo"] ) diff --git a/backend/capellacollab/tools/crud.py b/backend/capellacollab/tools/crud.py index 9692e9b8ec..e06bdfd428 100644 --- a/backend/capellacollab/tools/crud.py +++ b/backend/capellacollab/tools/crud.py @@ -7,7 +7,7 @@ from sqlalchemy import exc, orm from capellacollab.core import database -from capellacollab.tools.integrations import models as integrations_models +from capellacollab.tools import models as tools_models from . import exceptions, models @@ -35,43 +35,21 @@ def get_tool_by_name( def create_tool( - db: orm.Session, tool: models.DatabaseTool + db: orm.Session, tool: models.CreateTool ) -> models.DatabaseTool: - tool.integrations = integrations_models.DatabaseToolIntegrations( - pure_variants=False, t4c=False, jupyter=False + database_tool = tools_models.DatabaseTool( + name=tool.name, integrations=tool.integrations ) - db.add(tool) + db.add(database_tool) db.commit() - return tool - - -def create_tool_with_name( - db: orm.Session, tool_name: str -) -> models.DatabaseTool: - return create_tool( - db, tool=models.DatabaseTool(name=tool_name, docker_image_template="") - ) + return database_tool -def update_tool_name( - db: orm.Session, tool: models.DatabaseTool, tool_name: str +def update_tool( + db: orm.Session, tool: models.DatabaseTool, updated_tool: models.CreateTool ) -> models.DatabaseTool: - tool.name = tool_name - db.commit() - return tool - - -def update_tool_dockerimages( - db: orm.Session, - tool: models.DatabaseTool, - patch_tool: models.PatchToolDockerimage, -) -> models.DatabaseTool: - if patch_tool.persistent: - tool.docker_image_template = patch_tool.persistent - if patch_tool.readonly: - tool.readonly_docker_image_template = patch_tool.readonly - if patch_tool.backup: - tool.docker_image_backup_template = patch_tool.backup + tool.name = updated_tool.name + tool.integrations = updated_tool.integrations db.commit() return tool @@ -141,9 +119,10 @@ def get_version_by_tool_id_version_name( def update_version( db: orm.Session, version: models.DatabaseVersion, - patch_version: models.UpdateToolVersion, + updated_version: models.CreateToolVersion, ) -> models.DatabaseVersion: - database.patch_database_with_pydantic_object(version, patch_version) + version.name = updated_version.name + version.config = updated_version.config db.commit() return version @@ -152,14 +131,11 @@ def update_version( def create_version( db: orm.Session, tool_id: int, - name: str, - is_recommended: bool = False, - is_deprecated: bool = False, + tool_version: models.CreateToolVersion, ) -> models.DatabaseVersion: version = models.DatabaseVersion( - name=name, - is_recommended=is_recommended, - is_deprecated=is_deprecated, + name=tool_version.name, + config=tool_version.config, tool_id=tool_id, ) db.add(version) @@ -222,6 +198,17 @@ def get_natures_by_tool_id( ) +def update_nature( + db: orm.Session, + nature: models.DatabaseNature, + updated_version: models.CreateToolNature, +) -> models.DatabaseNature: + nature.name = updated_version.name + + db.commit() + return nature + + def create_nature( db: orm.Session, tool_id: int, name: str ) -> models.DatabaseNature: @@ -261,7 +248,7 @@ def get_backup_image_for_tool_version(db: orm.Session, version_id: int) -> str: if not (version := get_version_by_id(db, version_id)): raise exceptions.ToolVersionNotFoundError(version_id) - backup_image_template = version.tool.docker_image_backup_template + backup_image_template = version.config[""] if not backup_image_template: raise exceptions.ToolImageNotFoundError( diff --git a/backend/capellacollab/tools/integrations/__init__.py b/backend/capellacollab/tools/integrations/__init__.py deleted file mode 100644 index 04412280d8..0000000000 --- a/backend/capellacollab/tools/integrations/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 diff --git a/backend/capellacollab/tools/integrations/crud.py b/backend/capellacollab/tools/integrations/crud.py deleted file mode 100644 index c8ee370877..0000000000 --- a/backend/capellacollab/tools/integrations/crud.py +++ /dev/null @@ -1,21 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from sqlalchemy import orm - -from capellacollab.core import database - -from . import models - - -def update_integrations( - db: orm.Session, - integrations: models.DatabaseToolIntegrations, - patch_integrations: models.PatchToolIntegrations, -) -> models.DatabaseToolIntegrations: - database.patch_database_with_pydantic_object( - integrations, patch_integrations - ) - - db.commit() - return integrations diff --git a/backend/capellacollab/tools/integrations/models.py b/backend/capellacollab/tools/integrations/models.py deleted file mode 100644 index 9615d5663b..0000000000 --- a/backend/capellacollab/tools/integrations/models.py +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import typing as t - -import pydantic -import sqlalchemy as sa -from sqlalchemy import orm - -from capellacollab.core import database - -if t.TYPE_CHECKING: - from capellacollab.tools.models import DatabaseTool - - -class ToolIntegrations(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) - - t4c: bool - pure_variants: bool - jupyter: bool - - -class PatchToolIntegrations(pydantic.BaseModel): - t4c: bool | None = None - pure_variants: bool | None = None - jupyter: bool | None = None - - -class DatabaseToolIntegrations(database.Base): - __tablename__ = "tool_integrations" - - id: orm.Mapped[int] = orm.mapped_column(primary_key=True) - - tool_id: orm.Mapped[int] = orm.mapped_column(sa.ForeignKey("tools.id")) - tool: orm.Mapped[DatabaseTool] = orm.relationship( - back_populates="integrations" - ) - - t4c: orm.Mapped[bool] = orm.mapped_column(default=False) - pure_variants: orm.Mapped[bool] = orm.mapped_column(default=False) - jupyter: orm.Mapped[bool] = orm.mapped_column(default=False) diff --git a/backend/capellacollab/tools/integrations/routes.py b/backend/capellacollab/tools/integrations/routes.py deleted file mode 100644 index a5953e333e..0000000000 --- a/backend/capellacollab/tools/integrations/routes.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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.users import models as users_models - -from .. import injectables as tools_injectables -from .. import models as tools_models -from . import crud, models - -router = fastapi.APIRouter( - dependencies=[ - fastapi.Depends( - auth_injectables.RoleVerification( - required_role=users_models.Role.ADMIN - ) - ) - ] -) - - -@router.put("", response_model=models.ToolIntegrations) -def update_integrations( - body: models.PatchToolIntegrations, - tool: tools_models.DatabaseTool = fastapi.Depends( - tools_injectables.get_existing_tool - ), - db: orm.Session = fastapi.Depends(database.get_db), -) -> models.DatabaseToolIntegrations: - return crud.update_integrations(db, tool.integrations, body) diff --git a/backend/capellacollab/tools/models.py b/backend/capellacollab/tools/models.py index ec682ad095..892385a8d6 100644 --- a/backend/capellacollab/tools/models.py +++ b/backend/capellacollab/tools/models.py @@ -4,28 +4,48 @@ from __future__ import annotations -import typing as t - import pydantic import sqlalchemy as sa from sqlalchemy import orm from capellacollab.core import database -from capellacollab.tools.integrations import models as integrations_models +from capellacollab.core.database import decorator + +DOCKER_IMAGE_PATTERN = r"^[a-zA-Z0-9][a-zA-Z0-9_\-/.:$]*$" + + +class ToolIntegrations(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True, extra="forbid") -if t.TYPE_CHECKING: - from .integrations.models import DatabaseToolIntegrations + t4c: bool = pydantic.Field( + default=False, + description=( + "Enables support for TeamForCapella." + "If enabled, TeamForCapella repositories will be shown as model sources for corresponding models." + "Also, session tokens are created for corresponding sessions." + "Please refer to the documentation for more details." + ), + ) + pure_variants: bool = pydantic.Field( + default=False, + description=( + "Enables support for pure::variants." + "If enabled and the restrictions are met, pure::variants license secrets & information will be mounted to containers." + "Please refer to the documentation for more details." + ), + ) + jupyter: bool = pydantic.Field( + default=False, + description="Activate if the used tool is Jupyter.", + ) class DatabaseTool(database.Base): __tablename__ = "tools" - id: orm.Mapped[int] = orm.mapped_column(primary_key=True) + id: orm.Mapped[int] = orm.mapped_column(primary_key=True, index=True) name: orm.Mapped[str] - docker_image_template: orm.Mapped[str] - docker_image_backup_template: orm.Mapped[str | None] - readonly_docker_image_template: orm.Mapped[str | None] versions: orm.Mapped[list[DatabaseVersion]] = orm.relationship( back_populates="tool" @@ -34,8 +54,89 @@ class DatabaseTool(database.Base): back_populates="tool" ) - integrations: orm.Mapped[DatabaseToolIntegrations] = orm.relationship( - back_populates="tool", uselist=False + integrations: orm.Mapped[ToolIntegrations] = orm.mapped_column( + decorator.PydanticDecorator(ToolIntegrations), nullable=False + ) + + +class ReadOnlySessionToolConfiguration(pydantic.BaseModel): + image: str | None = pydantic.Field( + default=None, + pattern=DOCKER_IMAGE_PATTERN, + examples=[ + "docker.io/hello-world:latest", + "ghcr.io/dsd-dbs/capella-dockerimages/capella/readonly:{version}-main", + ], + description=( + "Docker image, which is used for read-only sessions. " + "If set to None, read-only session support will be disabled for this tool version. " + "You can use '{version}' in the image, which will be replaced with the version name of the tool. " + "Always use tags to prevent breaking updates. " + ), + ) + + +class PersistentSessionToolConfiguration(pydantic.BaseModel): + image: str = pydantic.Field( + default="docker.io/hello-world:latest", + pattern=DOCKER_IMAGE_PATTERN, + examples=[ + "docker.io/hello-world:latest", + "ghcr.io/dsd-dbs/capella-dockerimages/capella/remote:{version}-main", + ], + description=( + "Docker image, which is used for backup pipelines. " + "If set to None, persistent session support will be disabled for this tool version. " + "You can use '{version}' in the image, which will be replaced with the version name of the tool. " + "Always use tags to prevent breaking updates. " + ), + ) + + +class ToolBackupConfiguration(pydantic.BaseModel): + image: str = pydantic.Field( + default="docker.io/hello-world:latest", + pattern=DOCKER_IMAGE_PATTERN, + examples=[ + "docker.io/hello-world:latest", + "ghcr.io/dsd-dbs/capella-dockerimages/capella/base:{version}-main", + ], + description=( + "Docker image, which is used for backup pipelines. " + "If set to None, it's will no longer be possible to create a spawn backup pipelines for this tool version. " + "You can use '{version}' in the image, which will be replaced with the version name of the tool. " + "Always use tags to prevent breaking updates. " + ), + ) + + +class SessionToolConfiguration(pydantic.BaseModel): + persistent: PersistentSessionToolConfiguration = pydantic.Field( + default=PersistentSessionToolConfiguration() + ) + read_only: ReadOnlySessionToolConfiguration = pydantic.Field( + default=ReadOnlySessionToolConfiguration() + ) + + +class ToolVersionConfiguration(pydantic.BaseModel): + is_recommended: bool = pydantic.Field( + default=False, + description="Version will be displayed as recommended.", + ) + is_deprecated: bool = pydantic.Field( + default=False, + description="Version will be displayed as deprecated.", + ) + + sessions: SessionToolConfiguration = pydantic.Field( + default=SessionToolConfiguration(), + description="Configuration for sessions.", + ) + + backups: ToolBackupConfiguration = pydantic.Field( + default=ToolBackupConfiguration(), + description="Configuration for the backup pipelines.", ) @@ -44,10 +145,11 @@ class DatabaseVersion(database.Base): __table_args__ = (sa.UniqueConstraint("tool_id", "name"),) id: orm.Mapped[int] = orm.mapped_column(primary_key=True) - name: orm.Mapped[str] - is_recommended: orm.Mapped[bool] - is_deprecated: orm.Mapped[bool] + + config: orm.Mapped[ToolVersionConfiguration] = orm.mapped_column( + decorator.PydanticDecorator(ToolVersionConfiguration) + ) tool_id: orm.Mapped[int | None] = orm.mapped_column( sa.ForeignKey("tools.id") @@ -70,55 +172,49 @@ class DatabaseNature(database.Base): tool: orm.Mapped[DatabaseTool] = orm.relationship(back_populates="natures") -class ToolBase(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class CreateTool(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True, extra="forbid") - id: int - name: str - integrations: integrations_models.ToolIntegrations + name: str = pydantic.Field(default="", min_length=2, max_length=30) + integrations: ToolIntegrations = pydantic.Field(default=ToolIntegrations()) -class ToolDockerimage(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) +class ToolBase(CreateTool, decorator.PydanticDatabaseModel): + pass - persistent: str = pydantic.Field( - ..., validation_alias="docker_image_template" - ) - readonly: str | None = pydantic.Field( - None, validation_alias="readonly_docker_image_template" - ) - backup: str | None = pydantic.Field( - None, validation_alias="docker_image_backup_template" - ) +class ToolConfiguration(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True, extra="forbid") -class PatchToolDockerimage(pydantic.BaseModel): - persistent: str | None = None - readonly: str | None = None - backup: str | None = None + name: str + versions: list[ToolVersionBase] + natures: list[ToolNatureBase] -class CreateToolVersion(pydantic.BaseModel): - name: str + integrations: ToolIntegrations + + sessions: None + backups: None class CreateToolNature(pydantic.BaseModel): - name: str + model_config = pydantic.ConfigDict(from_attributes=True, extra="forbid") + name: str = pydantic.Field(default="", min_length=2, max_length=30) -class UpdateToolVersion(pydantic.BaseModel): - name: str | None = None - is_recommended: bool | None = None - is_deprecated: bool | None = None +class CreateToolVersion(pydantic.BaseModel): + model_config = pydantic.ConfigDict(from_attributes=True, extra="forbid") -class ToolVersionBase(pydantic.BaseModel): - model_config = pydantic.ConfigDict(from_attributes=True) + name: str = pydantic.Field(default="", min_length=2, max_length=30) + + config: ToolVersionConfiguration = pydantic.Field( + default=ToolVersionConfiguration() + ) - id: int - name: str - is_recommended: bool - is_deprecated: bool + +class ToolVersionBase(CreateToolVersion, decorator.PydanticDatabaseModel): + pass class ToolVersionWithTool(ToolVersionBase): @@ -130,7 +226,3 @@ class ToolNatureBase(pydantic.BaseModel): id: int name: str - - -class CreateTool(pydantic.BaseModel): - name: str diff --git a/backend/capellacollab/tools/routes.py b/backend/capellacollab/tools/routes.py index 0b89b32072..11f7aa4244 100644 --- a/backend/capellacollab/tools/routes.py +++ b/backend/capellacollab/tools/routes.py @@ -12,9 +12,6 @@ from capellacollab.core import database from capellacollab.core import exceptions as core_exceptions from capellacollab.core.authentication import injectables as auth_injectables -from capellacollab.tools.integrations import ( - routes as tools_integrations_routes, -) from capellacollab.users import models as users_models from . import crud, injectables, models @@ -37,6 +34,11 @@ def get_tools( return crud.get_tools(db) +@router.get("/default") +def get_default_tool() -> models.CreateTool: + return models.CreateTool() + + @router.get("/{tool_id}", response_model=models.ToolBase) def get_tool_by_id( tool=fastapi.Depends(injectables.get_existing_tool), @@ -46,7 +48,7 @@ def get_tool_by_id( @router.post( "", - response_model=models.ToolNatureBase, + response_model=models.ToolBase, dependencies=[ fastapi.Depends( auth_injectables.RoleVerification( @@ -58,12 +60,12 @@ def get_tool_by_id( def create_tool( body: models.CreateTool, db: orm.Session = fastapi.Depends(database.get_db) ) -> models.DatabaseTool: - return crud.create_tool_with_name(db, body.name) + return crud.create_tool(db, body) @router.put( "/{tool_id}", - response_model=models.ToolNatureBase, + response_model=models.ToolBase, dependencies=[ fastapi.Depends( auth_injectables.RoleVerification( @@ -77,7 +79,7 @@ def update_tool( tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseTool: - return crud.update_tool_name(db, tool, body.name) + return crud.update_tool(db, tool, body) @router.delete( @@ -95,23 +97,21 @@ def delete_tool( tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), db: orm.Session = fastapi.Depends(database.get_db), ): - if tool.id == 1: - raise fastapi.HTTPException( - status_code=status.HTTP_403_FORBIDDEN, - detail={ - "reason": "The tool 'Capella' cannot be deleted.", - }, - ) - raise_when_tool_dependency_exist(db, tool) crud.delete_tool(db, tool) @router.get("/{tool_id}/versions", response_model=list[models.ToolVersionBase]) def get_tool_versions( - tool_id: int, db: orm.Session = fastapi.Depends(database.get_db) + tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), + db: orm.Session = fastapi.Depends(database.get_db), ) -> abc.Sequence[models.DatabaseVersion]: - return crud.get_versions_for_tool_id(db, tool_id) + return crud.get_versions_for_tool_id(db, tool.id) + + +@router.get("/{tool_id}/versions/default") +def get_default_tool_version() -> models.CreateToolVersion: + return models.CreateToolVersion() @router.post( @@ -130,10 +130,14 @@ def create_tool_version( tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseVersion: - return crud.create_version(db, tool.id, body.name) + if crud.get_version_by_tool_id_version_name(db, tool.id, body.name): + raise core_exceptions.ResourceAlreadyExistsError( + "tool version", "name" + ) + return crud.create_version(db, tool.id, body) -@router.patch( +@router.put( "/{tool_id}/versions/{version_id}", response_model=models.ToolVersionBase, dependencies=[ @@ -144,13 +148,21 @@ def create_tool_version( ) ], ) -def patch_tool_version( - body: models.UpdateToolVersion, +def update_tool_version( + body: models.CreateToolVersion, + tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), version: models.DatabaseVersion = fastapi.Depends( injectables.get_exisiting_tool_version ), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseVersion: + existing_version = crud.get_version_by_tool_id_version_name( + db, tool.id, body.name + ) + if existing_version and existing_version.id != version.id: + raise core_exceptions.ResourceAlreadyExistsError( + "tool version", "name" + ) return crud.update_version(db, version, body) @@ -177,9 +189,15 @@ def delete_tool_version( @router.get("/{tool_id}/natures", response_model=list[models.ToolNatureBase]) def get_tool_natures( - tool_id: int, db: orm.Session = fastapi.Depends(database.get_db) + tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), + db: orm.Session = fastapi.Depends(database.get_db), ) -> abc.Sequence[models.DatabaseNature]: - return crud.get_natures_by_tool_id(db, tool_id) + return crud.get_natures_by_tool_id(db, tool.id) + + +@router.get("/{tool_id}/natures/default") +def get_default_tool_nature() -> models.CreateToolNature: + return models.CreateToolNature() @router.post( @@ -194,16 +212,18 @@ def get_tool_natures( ], ) def create_tool_nature( - tool_id: int, body: models.CreateToolNature, + tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), db: orm.Session = fastapi.Depends(database.get_db), ) -> models.DatabaseNature: - return crud.create_nature(db, tool_id, body.name) + if crud.get_nature_by_name(db, tool, body.name): + raise core_exceptions.ResourceAlreadyExistsError("tool nature", "name") + return crud.create_nature(db, tool.id, body.name) -@router.delete( +@router.put( "/{tool_id}/natures/{nature_id}", - status_code=204, + response_model=models.ToolNatureBase, dependencies=[ fastapi.Depends( auth_injectables.RoleVerification( @@ -212,36 +232,23 @@ def create_tool_nature( ) ], ) -def delete_tool_nature( +def update_tool_nature( + body: models.CreateToolNature, + tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), nature: models.DatabaseNature = fastapi.Depends( injectables.get_exisiting_tool_nature ), db: orm.Session = fastapi.Depends(database.get_db), -): - raise_when_tool_nature_dependency_exist(db, nature) - crud.delete_nature(db, nature) - - -@router.get( - "/{tool_id}/dockerimages", - response_model=models.ToolDockerimage, - dependencies=[ - fastapi.Depends( - auth_injectables.RoleVerification( - required_role=users_models.Role.ADMIN - ) - ) - ], -) -def get_dockerimages( - tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), -) -> models.DatabaseTool: - return tool +) -> models.DatabaseNature: + existing_nature = crud.get_nature_by_name(db, tool, body.name) + if existing_nature and existing_nature.id != nature.id: + raise core_exceptions.ResourceAlreadyExistsError("tool nature", "name") + return crud.update_nature(db, nature, body) -@router.put( - "/{tool_id}/dockerimages", - response_model=models.ToolDockerimage, +@router.delete( + "/{tool_id}/natures/{nature_id}", + status_code=204, dependencies=[ fastapi.Depends( auth_injectables.RoleVerification( @@ -250,17 +257,14 @@ def get_dockerimages( ) ], ) -def update_dockerimages( - body: models.PatchToolDockerimage, - tool: models.DatabaseTool = fastapi.Depends(injectables.get_existing_tool), +def delete_tool_nature( + nature: models.DatabaseNature = fastapi.Depends( + injectables.get_exisiting_tool_nature + ), db: orm.Session = fastapi.Depends(database.get_db), -) -> models.DatabaseTool: - return crud.update_tool_dockerimages(db, tool, body) - - -router.include_router( - tools_integrations_routes.router, prefix="/{tool_id}/integrations" -) +): + raise_when_tool_nature_dependency_exist(db, nature) + crud.delete_nature(db, nature) def raise_when_tool_dependency_exist( diff --git a/docs/docs/admin/settings/tools/index.md b/docs/docs/admin/settings/tools/index.md index d771c96fef..de6c9fc15c 100644 --- a/docs/docs/admin/settings/tools/index.md +++ b/docs/docs/admin/settings/tools/index.md @@ -5,19 +5,39 @@ # Tools Management -Starting with `v2.0.0`, there is a support for general tools. This allows the -use of many different `Eclipse` based tools (and even more tools if they are -dockerized). +Tools are a central element of the Collaboration Manager. While Capella remains +a core tool of the platform, we have generic tool support. This not only allows +administrators to use additional tools such as Eclipse, pure::variants or +Papyrus, but also to expand the platform with their own tools. + +Basically, a tool can be added if it can run in a Docker container and can be +reached via RDP. General web-based tool support is on our roadmap; currently +only Jupyter can be used as a web-based tool. + +Tools can be found in various places on the platform: + +- Models in projects are always assigned to a specific tool. +- Sessions are always started for a specific tool. + +Each tool has different versions and natures, which can be configured +individually. Since different versions can be enabled in parallel, it helps to +carry out complex migrations step by step. ## Managing Tools -The tool management page can be found at `Profile` > `Settings` > `Core` > -`Tools`. It support the following: +Tools are managed by the platform administrator. The tools management page +allows the administrator to add, edit, and delete tools. + +The tool management page can be found at `Menu` > `Settings` > `Tools`. Here, +you'll find several YAML editors. + +To change the configuration, edit the YAML configuration in the corresponding +editor. Once you're done, click `Save`. We run several validation rules during +saving to avoid configuration errors. You'll be notified about the save result +in the bottom left corner via a notification. -- Change the docker images for - - persistent sessions - - read-only sessions - - backups -- Set versions and natures of tools +The `id` entry is only displayed for reference and cannot be changed, any +changes of the `id` are ignored. When creating a new version or a new nature, +the ID will be auto-assigned. -More information will follow in the future! +To see all available options, please refer to the API documentation. diff --git a/frontend/angular.json b/frontend/angular.json index d988812df0..a5bda9497c 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -35,7 +35,15 @@ "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", - "assets": ["src/favicon.ico", "src/assets"], + "assets": [ + "src/favicon.ico", + "src/assets", + { + "glob": "codicon.ttf", + "input": "./node_modules/monaco-editor/min/vs/base/browser/ui/codicons/codicon/", + "output": "/" + } + ], "styles": [ "node_modules/monaco-editor/min/vs/editor/editor.main.css", "src/custom-theme.scss", @@ -129,7 +137,15 @@ "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.spec.json", "karmaConfig": "karma.conf.js", - "assets": ["src/favicon.ico", "src/assets"], + "assets": [ + "src/favicon.ico", + "src/assets", + { + "glob": "codicon.ttf", + "input": "./node_modules/monaco-editor/min/vs/base/browser/ui/codicons/codicon/", + "output": "/" + } + ], "styles": [ "node_modules/monaco-editor/min/vs/editor/editor.main.css", "src/styles.css" diff --git a/frontend/package-lock.json b/frontend/package-lock.json index fb747992ba..6c30a9b80f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -36,6 +36,7 @@ "semver": "^7.5.4", "slugify": "^1.6.6", "tslib": "^2.6.2", + "uuid": "^9.0.1", "yaml": "^2.3.3", "zone.js": "~0.14.3" }, @@ -53,6 +54,7 @@ "@types/file-saver": "^2.0.7", "@types/jasmine": "~5.1.4", "@types/node": "^20.11.10", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "autoprefixer": "^10.4.17", @@ -4438,6 +4440,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.0.tgz", "integrity": "sha512-2yThA1Es98orMkpSLVqlDZAMPK3jHJhifP2gnNUdk1754uZ8yI5c+ulCoVG+WlntQA6MzhrURMXjSd9Z7dJ2/Q==", + "dev": true, "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", @@ -4453,6 +4456,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", + "dev": true, "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -4465,6 +4469,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -4473,6 +4478,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", + "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", @@ -4497,6 +4503,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.4.tgz", "integrity": "sha512-nr6/WezNzuYUppzXRaYu/W4aT5rLxdXqEFupbh6e/ovlYFQ8hpu1UUPV3Ir/YTl+74iXl2ZOMlGzudh9ZPUchQ==", + "dev": true, "dependencies": { "@npmcli/promise-spawn": "^7.0.0", "lru-cache": "^10.0.1", @@ -4515,6 +4522,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "engines": { "node": ">=16" } @@ -4523,6 +4531,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -4531,6 +4540,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "dependencies": { "isexe": "^3.1.1" }, @@ -4592,6 +4602,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.0.0.tgz", "integrity": "sha512-OI2zdYBLhQ7kpNPaJxiflofYIpkNLi+lnGdzqUOfRmCF3r2l1nadcjtCYMJKv/Utm/ZtlffaUuTiAktPHbc17g==", + "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", "glob": "^10.2.2", @@ -4609,6 +4620,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -4630,6 +4642,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, "dependencies": { "lru-cache": "^10.0.1" }, @@ -4641,6 +4654,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -4649,6 +4663,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.1.tgz", "integrity": "sha512-P4KkF9jX3y+7yFUxgcUdDtLy+t4OlDGuEBLNs57AZsfSfg+uV6MLndqGpnl4831ggaEdXwR50XFoZP4VFtHolg==", + "dev": true, "dependencies": { "which": "^4.0.0" }, @@ -4660,6 +4675,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "engines": { "node": ">=16" } @@ -4668,6 +4684,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "dependencies": { "isexe": "^3.1.1" }, @@ -4682,6 +4699,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "dev": true, "dependencies": { "@npmcli/node-gyp": "^3.0.0", "@npmcli/package-json": "^5.0.0", @@ -4697,6 +4715,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "engines": { "node": ">=16" } @@ -4705,6 +4724,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "dependencies": { "isexe": "^3.1.1" }, @@ -5230,6 +5250,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz", "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==", + "dev": true, "dependencies": { "@sigstore/protobuf-specs": "^0.2.1" }, @@ -5241,6 +5262,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-0.2.0.tgz", "integrity": "sha512-THobAPPZR9pDH2CAvDLpkrYedt7BlZnsyxDe+Isq4ZmGfPy5juOFZq487vCU2EgKD7aHSiTfE/i7sN7aEdzQnA==", + "dev": true, "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -5257,6 +5279,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.1.tgz", "integrity": "sha512-U5sKQEj+faE1MsnLou1f4DQQHeFZay+V9s9768lw48J4pKykPj34rWyI1lsMOGJ3Mae47Ye6q3HAJvgXO21rkQ==", + "dev": true, "dependencies": { "@sigstore/bundle": "^2.1.1", "@sigstore/core": "^0.2.0", @@ -5271,6 +5294,7 @@ "version": "18.0.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -5293,6 +5317,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -5314,6 +5339,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -5322,6 +5348,7 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "dev": true, "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -5343,6 +5370,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -5351,6 +5379,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, "dependencies": { "minipass": "^7.0.3" }, @@ -5362,6 +5391,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz", "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==", + "dev": true, "dependencies": { "@sigstore/protobuf-specs": "^0.2.1", "tuf-js": "^2.2.0" @@ -5374,6 +5404,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-0.1.0.tgz", "integrity": "sha512-2UzMNYAa/uaz11NhvgRnIQf4gpLTJ59bhb8ESXaoSS5sxedfS+eLak8bsdMc+qpNQfITUTFoSKFx5h8umlRRiA==", + "dev": true, "dependencies": { "@sigstore/bundle": "^2.1.1", "@sigstore/core": "^0.2.0", @@ -5453,6 +5484,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -5461,6 +5493,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.0.tgz", "integrity": "sha512-c8nj8BaOExmZKO2DXhDfegyhSGcG9E/mPN3U13L+/PsoWm1uaGiHHjxqSHQiasDBQwDA3aHuw9+9spYAP1qvvg==", + "dev": true, "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^9.0.3" @@ -5702,6 +5735,12 @@ "@types/node": "*" } }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true + }, "node_modules/@types/ws": { "version": "8.5.10", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", @@ -6303,6 +6342,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -6390,6 +6430,7 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "dev": true, "dependencies": { "debug": "^4.3.4" }, @@ -6521,6 +6562,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, "dependencies": { "color-convert": "^1.9.0" }, @@ -7419,6 +7461,7 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -7649,6 +7692,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, "dependencies": { "color-name": "1.1.3" } @@ -7656,7 +7700,8 @@ "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true }, "node_modules/color-support": { "version": "1.1.3", @@ -8981,6 +9026,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -10588,6 +10634,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, "engines": { "node": ">=4" } @@ -10928,6 +10975,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", + "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -13553,6 +13601,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.0.1.tgz", "integrity": "sha512-gg3/bHehQfZivQVfqIyy8wTdSymF9yTyP4CJifK73imyNMU8AIGQE2pUa7dNWfmMeG9cDVF2eehiRMv0LC1iAg==", + "dev": true, "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", @@ -13588,6 +13637,7 @@ "version": "18.0.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -13610,6 +13660,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -13631,6 +13682,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "engines": { "node": ">=16" } @@ -13639,6 +13691,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -13647,6 +13700,7 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "dev": true, "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -13668,6 +13722,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -13676,6 +13731,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, "dependencies": { "minipass": "^7.0.3" }, @@ -13687,6 +13743,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "dependencies": { "isexe": "^3.1.1" }, @@ -13713,6 +13770,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.0.tgz", "integrity": "sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA==", + "dev": true, "dependencies": { "abbrev": "^2.0.0" }, @@ -13727,6 +13785,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "dev": true, "dependencies": { "hosted-git-info": "^7.0.0", "is-core-module": "^2.8.1", @@ -13741,6 +13800,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, "dependencies": { "lru-cache": "^10.0.1" }, @@ -13752,6 +13812,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -14766,6 +14827,7 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "dev": true, "dependencies": { "hosted-git-info": "^7.0.0", "proc-log": "^3.0.0", @@ -14780,6 +14842,7 @@ "version": "7.0.1", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "dev": true, "dependencies": { "lru-cache": "^10.0.1" }, @@ -14791,6 +14854,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -14799,6 +14863,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, "dependencies": { "ignore-walk": "^6.0.4" }, @@ -14810,6 +14875,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, "dependencies": { "npm-install-checks": "^6.0.0", "npm-normalize-package-bin": "^3.0.0", @@ -14824,6 +14890,7 @@ "version": "16.1.0", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.1.0.tgz", "integrity": "sha512-PQCELXKt8Azvxnt5Y85GseQDJJlglTFM9L9U9gkv2y4e9s0k3GVDdOx3YoB6gm2Do0hlkzC39iCGXby+Wve1Bw==", + "dev": true, "dependencies": { "make-fetch-happen": "^13.0.0", "minipass": "^7.0.2", @@ -14841,6 +14908,7 @@ "version": "18.0.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -14863,6 +14931,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -14884,6 +14953,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -14892,6 +14962,7 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "dev": true, "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -14913,6 +14984,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -14921,6 +14993,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, "dependencies": { "minipass": "^7.0.3" }, @@ -17992,6 +18065,7 @@ "version": "17.0.4", "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.4.tgz", "integrity": "sha512-eGdLHrV/g5b5MtD5cTPyss+JxOlaOloSMG3UwPMAvL8ywaLJ6beONPF40K4KKl/UI6q5hTKCJq5rCu8tkF+7Dg==", + "dev": true, "dependencies": { "@npmcli/git": "^5.0.0", "@npmcli/installed-package-contents": "^2.0.1", @@ -18023,6 +18097,7 @@ "version": "18.0.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -18045,6 +18120,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -18066,6 +18142,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -18074,6 +18151,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -18082,6 +18160,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, "dependencies": { "minipass": "^7.0.3" }, @@ -19094,6 +19173,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.0.tgz", "integrity": "sha512-uL4Z10OKV4p6vbdvIXB+OzhInYtIozl/VxUBPgNkBuUi2DeRonnuspmaVAMcrkmfjKGNmRndyQAbE7/AmzGwFg==", + "dev": true, "dependencies": { "glob": "^10.2.2", "json-parse-even-better-errors": "^3.0.0", @@ -19120,6 +19200,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -20011,6 +20092,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.0.tgz", "integrity": "sha512-fcU9clHwEss2/M/11FFM8Jwc4PjBgbhXoNskoK5guoK0qGQBSeUbQZRJ+B2fDFIvhyf0gqCaPrel9mszbhAxug==", + "dev": true, "dependencies": { "@sigstore/bundle": "^2.1.1", "@sigstore/core": "^0.2.0", @@ -20104,6 +20186,15 @@ "websocket-driver": "^0.7.4" } }, + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/socks": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", @@ -20586,6 +20677,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, "dependencies": { "has-flag": "^3.0.0" }, @@ -20888,7 +20980,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/thenify": { "version": "3.3.1", @@ -21084,6 +21177,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.0.tgz", "integrity": "sha512-ZSDngmP1z6zw+FIkIBjvOp/II/mIub/O7Pp12j1WNsiCpg5R5wAc//i555bBQsE44O94btLt0xM/Zr2LQjwdCg==", + "dev": true, "dependencies": { "@tufjs/models": "2.0.0", "debug": "^4.3.4", @@ -21097,6 +21191,7 @@ "version": "18.0.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.2.tgz", "integrity": "sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw==", + "dev": true, "dependencies": { "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", @@ -21119,6 +21214,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "dev": true, "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^2.3.5", @@ -21140,6 +21236,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "dev": true, "engines": { "node": "14 || >=16.14" } @@ -21148,6 +21245,7 @@ "version": "13.0.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz", "integrity": "sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A==", + "dev": true, "dependencies": { "@npmcli/agent": "^2.0.0", "cacache": "^18.0.0", @@ -21169,6 +21267,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "dev": true, "engines": { "node": ">=16 || 14 >=14.17" } @@ -21177,6 +21276,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, "dependencies": { "minipass": "^7.0.3" }, @@ -21563,10 +21663,13 @@ } }, "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, + "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" + ], "bin": { "uuid": "dist/bin/uuid" } diff --git a/frontend/package.json b/frontend/package.json index 97653ca9d1..58e435e482 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -41,8 +41,9 @@ "semver": "^7.5.4", "slugify": "^1.6.6", "tslib": "^2.6.2", - "zone.js": "~0.14.3", - "yaml": "^2.3.3" + "uuid": "^9.0.1", + "yaml": "^2.3.3", + "zone.js": "~0.14.3" }, "devDependencies": { "@angular-builders/custom-webpack": "^17.0.0", @@ -58,11 +59,12 @@ "@types/file-saver": "^2.0.7", "@types/jasmine": "~5.1.4", "@types/node": "^20.11.10", + "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", "autoprefixer": "^10.4.17", - "eslint": "^8.56.0", "css-loader": "^6.8.1", + "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-deprecation": "^2.0.0", "eslint-plugin-import": "^2.29.1", @@ -75,13 +77,13 @@ "karma-coverage-istanbul-reporter": "^3.0.3", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "^2.1.0", + "monaco-editor-webpack-plugin": "^7.1.0", "postcss": "^8.4.33", "prettier": "^3.2.4", "prettier-plugin-tailwindcss": "^0.5.11", + "style-loader": "^3.3.3", "tailwindcss": "^3.4.1", "typescript": "^5.2.2", - "webpack": "^5.90.0", - "monaco-editor-webpack-plugin": "^7.1.0", - "style-loader": "^3.3.3" + "webpack": "^5.90.0" } } diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index e65b7f90aa..86cf5a6f49 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -14,6 +14,7 @@ import { EditProjectMetadataComponent } from 'src/app/projects/project-detail/ed import { SessionComponent } from 'src/app/sessions/session/session.component'; import { ConfigurationSettingsComponent } from 'src/app/settings/core/configuration-settings/configuration-settings.component'; import { PipelinesOverviewComponent } from 'src/app/settings/core/pipelines-overview/pipelines-overview.component'; +import { CreateToolComponent } from 'src/app/settings/core/tools-settings/create-tool/create-tool.component'; import { BasicAuthTokenComponent } from 'src/app/users/basic-auth-token/basic-auth-token.component'; import { UsersProfileComponent } from 'src/app/users/users-profile/users-profile.component'; import { EventsComponent } from './events/events.component'; @@ -357,7 +358,7 @@ const routes: Routes = [ { path: 'create', data: { breadcrumb: 'Create Tool' }, - component: ToolDetailsComponent, + component: CreateToolComponent, }, ], }, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 9815b063a0..19c171c72c 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -51,6 +51,7 @@ import { HighlightPipeTransform, ModelDiagramCodeBlockComponent, } from 'src/app/projects/models/diagrams/model-diagram-dialog/model-diagram-code-block/model-diagram-code-block.component'; +import { CreateToolComponent } from 'src/app/settings/core/tools-settings/create-tool/create-tool.component'; import { BasicAuthTokenComponent } from 'src/app/users/basic-auth-token/basic-auth-token.component'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; @@ -175,6 +176,7 @@ import { UsersProfileComponent } from './users/users-profile/users-profile.compo CreateReadonlySessionComponent, CreateReadonlySessionDialogComponent, CreateT4cModelNewRepositoryComponent, + CreateToolComponent, DeleteGitSettingsDialogComponent, DeleteSessionDialogComponent, DisplayValueComponent, diff --git a/frontend/src/app/helpers/editor/editor.component.html b/frontend/src/app/helpers/editor/editor.component.html index 617d466bad..57de630a25 100644 --- a/frontend/src/app/helpers/editor/editor.component.html +++ b/frontend/src/app/helpers/editor/editor.component.html @@ -3,4 +3,8 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -
+
+ + + Hint: You can also save the content by pressing 'CTRL' + 'S' + diff --git a/frontend/src/app/helpers/editor/editor.component.ts b/frontend/src/app/helpers/editor/editor.component.ts index 8edda593c7..d1dff2d85b 100644 --- a/frontend/src/app/helpers/editor/editor.component.ts +++ b/frontend/src/app/helpers/editor/editor.component.ts @@ -4,39 +4,49 @@ */ import { + AfterViewInit, Component, + ElementRef, EventEmitter, HostListener, Input, NgZone, Output, + ViewChild, } from '@angular/core'; import * as monaco from 'monaco-editor'; -import { MetadataService } from 'src/app/general/metadata/metadata.service'; import { ToastService } from 'src/app/helpers/toast/toast.service'; -import { ConfigurationSettingsService } from 'src/app/settings/core/configuration-settings/configuration-settings.service'; +import { v4 as uuidv4 } from 'uuid'; import { stringify, parse, YAMLParseError } from 'yaml'; @Component({ selector: 'app-editor', templateUrl: './editor.component.html', }) -export class EditorComponent { +export class EditorComponent implements AfterViewInit { + @Input() + height = '400px'; + + @Input() + // Helps to identify the editor in the DOM + context = uuidv4(); + private editor?: monaco.editor.IStandaloneCodeEditor = undefined; intialValue = 'Loading...'; + @ViewChild('editorRef') editorRef: ElementRef | undefined; + @Output() submitted = new EventEmitter(); constructor( private ngZone: NgZone, - private configurationSettingsService: ConfigurationSettingsService, private toastService: ToastService, - private metadataService: MetadataService, + private el: ElementRef, ) {} - ngOnInit() { + ngAfterViewInit(): void { this.ngZone.runOutsideAngular(() => { this.initMonaco(); }); @@ -55,17 +65,28 @@ export class EditorComponent { } submitValue() { - if (!this.editor?.getValue()) { + const editorValue = this.editor?.getValue(); + + if (!editorValue) { this.toastService.showError( 'Configuration is empty', "The configuration editor doesn't contain any content. Make sure to enter a valid YAML configuration.", ); return; } + + if (editorValue === 'Loading...') { + this.toastService.showError( + 'Configuration is still loading', + 'The configuration editor is still loading. Please wait a moment.', + ); + return; + } + let jsonValue = ''; try { - jsonValue = parse(this.editor?.getValue()); + jsonValue = parse(editorValue); } catch (e) { if (e instanceof YAMLParseError) { this.toastService.showError('YAML parsing error', e.message); @@ -84,9 +105,11 @@ export class EditorComponent { private initMonaco() { const configModel = monaco.editor.createModel(this.intialValue, 'yaml'); - this.editor = monaco.editor.create(document.getElementById('editor')!, { + this.editor = monaco.editor.create(this.editorRef?.nativeElement, { value: 'Loading...', language: 'yaml', + minimap: { enabled: false }, + overviewRulerBorder: false, scrollBeyondLastLine: false, model: configModel, automaticLayout: true, @@ -95,10 +118,12 @@ export class EditorComponent { @HostListener('document:keydown', ['$event']) saveHandler(event: KeyboardEvent) { - if ((event.metaKey || event.ctrlKey) && event.key === 's') { - event.preventDefault(); - event.stopPropagation(); - this.submitValue(); + if (this.el.nativeElement.contains(event.target)) { + if ((event.metaKey || event.ctrlKey) && event.key === 's') { + event.preventDefault(); + event.stopPropagation(); + this.submitValue(); + } } } } diff --git a/frontend/src/app/settings/core/configuration-settings/configuration-settings.component.html b/frontend/src/app/settings/core/configuration-settings/configuration-settings.component.html index 4daaa3dcb0..a5c24877f2 100644 --- a/frontend/src/app/settings/core/configuration-settings/configuration-settings.component.html +++ b/frontend/src/app/settings/core/configuration-settings/configuration-settings.component.html @@ -3,7 +3,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> - +
+ +
+ + diff --git a/frontend/src/app/settings/core/tools-settings/create-tool/create-tool.component.ts b/frontend/src/app/settings/core/tools-settings/create-tool/create-tool.component.ts new file mode 100644 index 0000000000..868a2e7103 --- /dev/null +++ b/frontend/src/app/settings/core/tools-settings/create-tool/create-tool.component.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, ViewChild } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { EditorComponent } from 'src/app/helpers/editor/editor.component'; +import { ToastService } from 'src/app/helpers/toast/toast.service'; +import { ToolService } from 'src/app/settings/core/tools-settings/tool.service'; + +@Component({ + selector: 'app-create-tool', + templateUrl: './create-tool.component.html', +}) +export class CreateToolComponent { + @ViewChild(EditorComponent) editor: EditorComponent | undefined; + + constructor( + private toolService: ToolService, + private toastService: ToastService, + private router: Router, + private route: ActivatedRoute, + ) { + this.toolService.getDefaultTool().subscribe((tool) => { + this.editor!.value = tool; + }); + } + + submitValue(value: any): void { + delete value.id; + this.toolService.createTool(value).subscribe((tool) => { + this.toastService.showSuccess( + 'Tool created', + `The tool with the name '${tool.name}' has been created successfully.`, + ); + this.router.navigate(['../../tool', tool.id], { relativeTo: this.route }); + }); + } +} diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-deletion-dialog/tool-deletion-dialog.component.html b/frontend/src/app/settings/core/tools-settings/tool-details/tool-deletion-dialog/tool-deletion-dialog.component.html index 0b0947bfe7..ef55904444 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-deletion-dialog/tool-deletion-dialog.component.html +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-deletion-dialog/tool-deletion-dialog.component.html @@ -15,8 +15,12 @@

Remove Tool

The deletion cannot be undone!

Please remove all models with this tool before deletion.
-
- - -
-
- - -
-
- -
- + +
+

Common

+
- - - - +
+ + + +
+
+ + diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-details.component.ts b/frontend/src/app/settings/core/tools-settings/tool-details/tool-details.component.ts index d61c694f44..7137748b13 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-details.component.ts +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-details.component.ts @@ -3,21 +3,15 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component } from '@angular/core'; -import { - AbstractControl, - FormControl, - FormGroup, - ValidationErrors, - ValidatorFn, - Validators, -} from '@angular/forms'; +import { Component, ViewChild } from '@angular/core'; + import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; -import { combineLatest, filter, map, mergeMap, of, switchMap, tap } from 'rxjs'; +import { filter, map, mergeMap, tap } from 'rxjs'; import { BreadcrumbsService } from 'src/app/general/breadcrumbs/breadcrumbs.service'; +import { EditorComponent } from 'src/app/helpers/editor/editor.component'; import { ToastService } from 'src/app/helpers/toast/toast.service'; -import { Tool, ToolDockerimages, ToolService } from '../tool.service'; +import { Tool, ToolService } from '../tool.service'; import { ToolDeletionDialogComponent } from './tool-deletion-dialog/tool-deletion-dialog.component'; @Component({ @@ -26,30 +20,9 @@ import { ToolDeletionDialogComponent } from './tool-deletion-dialog/tool-deletio styleUrls: ['./tool-details.component.css'], }) export class ToolDetailsComponent { - editing = false; - existing = false; + @ViewChild(EditorComponent) editor: EditorComponent | undefined; selectedTool?: Tool; - dockerimages?: ToolDockerimages; - - public form = new FormGroup({ - name: new FormControl('', Validators.required), - dockerimages: new FormGroup({ - persistent: new FormControl('', [ - Validators.required, - Validators.maxLength(4096), - this.validDockerImageNameValidator(), - ]), - readonly: new FormControl('', [ - Validators.maxLength(4096), - this.validDockerImageNameValidator(), - ]), - backup: new FormControl('', [ - Validators.maxLength(4096), - this.validDockerImageNameValidator(), - ]), - }), - }); constructor( private route: ActivatedRoute, @@ -65,143 +38,34 @@ export class ToolDetailsComponent { .pipe( map((params) => params.toolID), filter((toolID) => toolID !== undefined), - mergeMap((toolID) => { - return combineLatest([ - of(toolID), - this.toolService._tools, - this.toolService.getDockerimagesForTool(toolID), - ]); - }), - tap(([_toolID, _tools, dockerimages]) => { - this.dockerimages = dockerimages; - }), - map(([toolID, tools, _dockerimages]) => { - return tools?.find((tool: Tool) => { - return tool.id == toolID; - }); - }), + mergeMap((toolID) => this.toolService.getToolByID(toolID)), ) .subscribe({ next: (tool) => { this.breadcrumbsService.updatePlaceholder({ tool }); - this.existing = true; this.selectedTool = tool; - this.updateForm(); + this.editor!.value = this.selectedTool; }, }); } - enableEditing(): void { - this.editing = true; - this.form.enable(); - } - - cancelEditing(): void { - this.editing = false; - this.form.disable(); - } - - updateForm(): void { - this.form.patchValue({ - name: this.selectedTool?.name, - dockerimages: this.dockerimages, - }); - this.cancelEditing(); - } - - validDockerImageNameValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - /* - Name components may contain lowercase letters, digits and separators. - A separator is defined as a period, one or two underscores, or one or more dashes. - A name component may not start or end with a separator. - - https://docs.docker.com/engine/reference/commandline/tag/#extended-description - - In addition, we allow $ (to use the $version syntax) and : for the tag. - */ - if ( - control.value && - !/^[a-zA-Z0-9][a-zA-Z0-9_\-/\.:\$]*$/.test(control.value) - ) { - return { validDockerImageNameInvalid: true }; - } - return {}; - }; - } - - create(): void { - if (this.form.valid) { - const name = this.form.controls.name.value!; - - this.toolService - .createTool(name) - .pipe( - tap((tool) => { - this.toastService.showSuccess( - 'Tool created', - `The tool with name ${tool.name} was created.`, - ); - - this.selectedTool = tool; - }), - switchMap((tool) => { - return this.toolService.updateDockerimagesForTool( - tool.id, - this.form.controls.dockerimages.value as ToolDockerimages, - ); - }), - tap((_) => { - this.toastService.showSuccess( - 'Docker images updated', - `The Docker images for the tool '${name}' were updated.`, - ); - }), - ) - .subscribe(() => { - this.router.navigate(['../..', 'tool', this.selectedTool?.id], { - relativeTo: this.route, - }); - }); - } - } - - update(): void { - if (this.form.valid) { - this.toolService - .updateTool(this.selectedTool!.id, this.form.controls.name.value!) - .pipe( - tap((tool) => { - this.toastService.showSuccess( - 'Tool updated', - `The tool name changed from '${this.selectedTool?.name}' to '${tool.name}'.`, - ); - }), - ) - .subscribe((tool) => { - this.selectedTool = tool; - }); - - this.toolService - .updateDockerimagesForTool( - this.selectedTool!.id, - this.form.controls.dockerimages.value as ToolDockerimages, - ) - .pipe( - tap((dockerimages) => { - this.dockerimages = dockerimages; - }), - ) - .subscribe((_) => { + submitValue(value: any): void { + delete value.id; + this.toolService + .updateTool(this.selectedTool!.id, value) + .pipe( + tap((tool) => { this.toastService.showSuccess( - 'Docker images for Tool updated', - `The Docker images for the tool with id ${ - this.selectedTool!.id - } were updated.`, + 'Tool updated', + `The configuration of the tool '${tool.name}' has been updated successfully.`, ); - this.cancelEditing(); - }); - } + this.editor!.value = tool; + this.breadcrumbsService.updatePlaceholder({ tool }); + }), + ) + .subscribe((tool) => { + this.selectedTool = tool; + }); } deleteTool(): void { @@ -221,12 +85,4 @@ export class ToolDetailsComponent { ); }); } - - submit(): void { - if (this.existing) { - this.update(); - } else { - this.create(); - } - } } diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.html b/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.html index 2f1db7782d..aed52ed9a8 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.html +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.html @@ -2,52 +2,67 @@ ~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors ~ SPDX-License-Identifier: Apache-2.0 --> -
-

Tool Natures

-
- - Nature name - - Please enter a tool nature name - Nature name is already used - -
- -
- - - extension -
-
{{ toolNature.name }}
-
-
-
-
-
-
-
- -
+ +
+

Tool Natures

-
+ + + + add_box + Add nature + + +
+ + +
+
+ + @if (toolNatures === undefined) { + + } + + @for (nature of toolNatures; track nature.id) { + + +
+ + + +
+
+ } +
+ diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.ts b/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.ts index cb239e2dc6..8a0c5d4ae5 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.ts +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-nature/tool-nature.component.ts @@ -3,18 +3,23 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, ViewChild } from '@angular/core'; import { - AbstractControl, - FormControl, - FormGroup, - ValidationErrors, - ValidatorFn, - Validators, -} from '@angular/forms'; -import { MatSelectionList } from '@angular/material/list'; -import { finalize, tap } from 'rxjs'; -import { Tool, ToolService, ToolNature } from '../../tool.service'; + Component, + Input, + QueryList, + ViewChild, + ViewChildren, +} from '@angular/core'; + +import { MatTabGroup } from '@angular/material/tabs'; +import { EditorComponent } from 'src/app/helpers/editor/editor.component'; +import { ToastService } from 'src/app/helpers/toast/toast.service'; +import { + Tool, + ToolService, + ToolNature, + CreateToolNature, +} from '../../tool.service'; @Component({ selector: 'app-tool-nature', @@ -29,7 +34,7 @@ export class ToolNatureComponent { if (this._tool && this._tool.id === value?.id) return; this._tool = value; - this.toolNatures = []; + this.toolNatures = undefined; this.toolService .getNaturesForTool(this._tool!.id) @@ -37,59 +42,79 @@ export class ToolNatureComponent { this.toolNatures = natures; }); } - toolNatures: ToolNature[] = []; - constructor(private toolService: ToolService) {} + @ViewChildren('editorRef') editorRefs: QueryList | undefined; + + @ViewChild('tabGroup', { static: false }) tabGroup: MatTabGroup | undefined; - @ViewChild('toolNatureList') toolNatureList!: MatSelectionList; + toolNatures: ToolNature[] | undefined = undefined; + + constructor( + private toolService: ToolService, + private toastService: ToastService, + ) {} + + ngAfterViewInit(): void { + this.toolService.getDefaultNature().subscribe((nature) => { + this.getEditorForContext('new')!.value = nature; + }); + } - get selectedToolNature() { - return this.toolNatureList.selectedOptions.selected[0].value; + getEditorForContext(context: string) { + return this.editorRefs?.find((editor) => editor.context === context); } - toolNatureForm = new FormGroup({ - name: new FormControl('', [ - Validators.required, - this.uniqueNameValidator(), - ]), - }); - - uniqueNameValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - return this.toolNatures.find((nature) => nature.name == control.value) - ? { toolVersionExists: true } - : null; - }; + resetValue(context: string) { + this.getEditorForContext(context)?.resetValue(); } - createToolNature(): void { - if (this.toolNatureForm.valid) { - this.toolNatureForm.disable(); - - this.toolService - .createNatureForTool( - this._tool!.id, - this.toolNatureForm.controls.name.value!, - ) - .pipe( - tap(() => { - this.toolNatureForm.reset(); - }), - finalize(() => { - this.toolNatureForm.enable(); - }), - ) - .subscribe((nature: ToolNature) => { - this.toolNatures.push(nature); - }); - } + submitValue(context: string) { + this.getEditorForContext(context)?.submitValue(); + } + + submittedValue(toolNature: ToolNature, value: ToolNature) { + const { id, ...valueWithoutID } = value; + this.toolService + .updateToolNature(this._tool!.id, toolNature.id, valueWithoutID) + .subscribe((toolNature: ToolNature) => { + this.toastService.showSuccess( + 'Tool nature updated', + `Successfully updated nature '${toolNature.name}' for tool '${this._tool!.name}'`, + ); + const natureIdx = this.toolNatures?.findIndex( + (v) => v.id === toolNature.id, + ); + + this.toolNatures![natureIdx!] = toolNature; + this.getEditorForContext(toolNature.id.toString())!.value = toolNature; + }); + } + + submittedNewToolNature(value: CreateToolNature) { + this.toolService + .createNatureForTool(this._tool!.id, value) + .subscribe((toolNature: ToolNature) => { + this.toastService.showSuccess( + 'Tool nature created', + `Successfully created nature '${toolNature.name}' for tool '${this._tool!.name}'`, + ); + this.toolNatures!.push(toolNature); + this.getEditorForContext('new')!.resetValue(); + this.jumpToLastTab(); + }); + } + + private jumpToLastTab() { + if (!this.tabGroup || !(this.tabGroup instanceof MatTabGroup)) return; + + this.tabGroup.selectedIndex = this.tabGroup._tabs.length; } removeToolNature(toolNature: ToolNature): void { this.toolService .deleteNatureForTool(this._tool!.id, toolNature) .subscribe(() => { - this.toolNatures = this.toolNatures.filter( + this.toolNatures = this.toolNatures!.filter( (nature) => nature.id !== toolNature.id, ); }); diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.css b/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.css index 73381358d3..8e9fc09a57 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.css +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.css @@ -3,13 +3,8 @@ * SPDX-License-Identifier: Apache-2.0 */ -.spinner { - display: flex; - justify-content: center; - align-items: center; - flex-grow: 10; -} - -mat-list-option { - height: 88px !important; +/* Remove min-width from mat-tab */ +::ng-deep.mat-tab-label, +::ng-deep.mat-tab-label-active { + min-width: 50px !important; } diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.html b/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.html index ff15feef46..507a96a89b 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.html +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.html @@ -2,75 +2,70 @@ ~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors ~ SPDX-License-Identifier: Apache-2.0 --> -
-

Tool Versions

- -
- - Version name - - Please enter a version name - Version name is already used - -
- -
- - - history -
-
{{ toolVersion.name }}
-
deprecated
-
recommended
+ +
+

Tool Versions

+
+ + + + add_box + Add version + + +
+ +
- - -
-
-
-
-
-
- Is deprecated + + @if (toolVersions === undefined) { + + } + + @for (version of toolVersions; track version.id) { + + +
+ +
-
- -
- -
-
-
+ Reset clear + + +
+ + } + + diff --git a/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.ts b/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.ts index dd505cd230..1a8ce778e3 100644 --- a/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.ts +++ b/frontend/src/app/settings/core/tools-settings/tool-details/tool-version/tool-version.component.ts @@ -3,19 +3,19 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Component, Input, OnInit } from '@angular/core'; import { - AbstractControl, - FormControl, - FormGroup, - ValidationErrors, - ValidatorFn, - Validators, -} from '@angular/forms'; -import { MatSelectionListChange } from '@angular/material/list'; -import { finalize, switchMap, tap } from 'rxjs'; + AfterViewInit, + Component, + Input, + QueryList, + ViewChild, + ViewChildren, +} from '@angular/core'; +import { MatTabGroup } from '@angular/material/tabs'; +import { EditorComponent } from 'src/app/helpers/editor/editor.component'; +import { ToastService } from 'src/app/helpers/toast/toast.service'; import { - PatchToolVersion, + CreateToolVersion, Tool, ToolService, ToolVersion, @@ -26,7 +26,7 @@ import { templateUrl: './tool-version.component.html', styleUrls: ['./tool-version.component.css'], }) -export class ToolVersionComponent implements OnInit { +export class ToolVersionComponent implements AfterViewInit { _tool?: Tool = undefined; @Input() @@ -34,7 +34,7 @@ export class ToolVersionComponent implements OnInit { if (this._tool && this._tool.id === value?.id) return; this._tool = value; - this.toolVersions = []; + this.toolVersions = undefined; this.toolService .getVersionsForTool(this._tool!.id) @@ -43,103 +43,79 @@ export class ToolVersionComponent implements OnInit { }); } - toolVersions: ToolVersion[] = []; + @ViewChildren('editorRef') editorRefs: QueryList | undefined; - constructor(private toolService: ToolService) {} + @ViewChild('tabGroup', { static: false }) tabGroup: MatTabGroup | undefined; - loadingMetadata = false; - toolVersionForm = new FormGroup({ - name: new FormControl('', [ - Validators.required, - this.uniqueNameValidator(), - ]), - }); + toolVersions: ToolVersion[] | undefined = undefined; - toolVersionMetadataForm = new FormGroup({ - isDeprecated: new FormControl(false), - isRecommended: new FormControl(false), - }); + constructor( + private toolService: ToolService, + private toastService: ToastService, + ) {} - selectedToolVersion: ToolVersion | undefined = undefined; + ngAfterViewInit(): void { + this.toolService.getDefaultVersion().subscribe((version) => { + this.getEditorForContext('new')!.value = version; + }); + } - isToolVersionSelected(toolVersion: ToolVersion) { - return toolVersion.id === this.selectedToolVersion?.id; + getEditorForContext(context: string) { + return this.editorRefs?.find((editor) => editor.context === context); } - onSelectionChange(event: MatSelectionListChange) { - this.selectedToolVersion = event.options[0].value; - this.toolVersionMetadataForm.patchValue({ - isDeprecated: this.selectedToolVersion?.is_deprecated, - isRecommended: this.selectedToolVersion?.is_recommended, - }); + resetValue(context: string) { + this.getEditorForContext(context)?.resetValue(); } - ngOnInit(): void { - this.onToolVersionMetadataFormChanges(); + submitValue(context: string) { + this.getEditorForContext(context)?.submitValue(); } - onToolVersionMetadataFormChanges(): void { - this.toolVersionMetadataForm.valueChanges - .pipe( - tap(() => { - this.loadingMetadata = true; - }), - switchMap(() => { - return this.toolService.patchToolVersion( - this._tool!.id, - this.selectedToolVersion!.id, - this.toolVersionMetadataForm.value as PatchToolVersion, - ); - }), - tap(() => { - this.loadingMetadata = false; - }), - ) - .subscribe((res) => { - const index = this.toolVersions.findIndex( - (version) => version.id === res.id, + submittedValue(toolVersion: ToolVersion, value: ToolVersion) { + const { id, ...valueWithoutID } = value; + this.toolService + .updateToolVersion(this._tool!.id, toolVersion.id, valueWithoutID) + .subscribe((toolVersion: ToolVersion) => { + this.toastService.showSuccess( + 'Tool version updated', + `Successfully updated version '${toolVersion.name}' for tool '${this._tool!.name}'`, + ); + const versionIdx = this.toolVersions?.findIndex( + (v) => v.id === toolVersion.id, ); - this.toolVersions[index] = res; - this.selectedToolVersion = res; + + this.toolVersions![versionIdx!] = toolVersion; + this.getEditorForContext(toolVersion.id.toString())!.value = + toolVersion; }); } - uniqueNameValidator(): ValidatorFn { - return (control: AbstractControl): ValidationErrors | null => { - return this.toolVersions.find((version) => version.name == control.value) - ? { toolVersionExists: true } - : null; - }; + submittedNewToolVersion(value: CreateToolVersion) { + this.toolService + .createVersionForTool(this._tool!.id, value) + .subscribe((toolVersion: ToolVersion) => { + this.toastService.showSuccess( + 'Tool version created', + `Successfully created version '${toolVersion.name}' for tool '${this._tool!.name}'`, + ); + this.toolVersions!.push(toolVersion); + this.getEditorForContext('new')!.resetValue(); + this.jumpToLastTab(); + }); } - createToolVersion(): void { - if (this.toolVersionForm.valid) { - this.toolVersionForm.disable(); - - this.toolService - .createVersionForTool( - this._tool!.id, - this.toolVersionForm.controls.name.value!, - ) - .pipe( - tap(() => { - this.toolVersionForm.reset(); - }), - finalize(() => { - this.toolVersionForm.enable(); - }), - ) - .subscribe((version: ToolVersion) => { - this.toolVersions.push(version); - }); - } + private jumpToLastTab() { + if (!this.tabGroup || !(this.tabGroup instanceof MatTabGroup)) return; + + this.tabGroup.selectedIndex = this.tabGroup._tabs.length; } removeToolVersion(toolVersion: ToolVersion): void { this.toolService .deleteVersionForTool(this._tool!.id, toolVersion) .subscribe(() => { - this.toolVersions = this.toolVersions.filter( + this.toolVersions = this.toolVersions!.filter( (version) => version.id !== toolVersion.id, ); }); diff --git a/frontend/src/app/settings/core/tools-settings/tool.service.ts b/frontend/src/app/settings/core/tools-settings/tool.service.ts index f43e2f8bfe..3a81f0b869 100644 --- a/frontend/src/app/settings/core/tools-settings/tool.service.ts +++ b/frontend/src/app/settings/core/tools-settings/tool.service.ts @@ -10,12 +10,12 @@ import { environment } from 'src/environments/environment'; export type CreateTool = { name: string; + integrations: ToolIntegrations; }; -export type Tool = CreateTool & { +export type Tool = { id: number; - integrations: ToolIntegrations; -}; +} & CreateTool; export type ToolIntegrations = { t4c: boolean | null; @@ -23,24 +23,25 @@ export type ToolIntegrations = { jupyter: boolean | null; }; -export type ToolVersion = { - id: number; +export type CreateToolVersion = { name: string; is_recommended: boolean; is_deprecated: boolean; }; +export type ToolVersion = { + id: number; +} & CreateToolVersion; + export type ToolVersionWithTool = ToolVersion & { tool: Tool }; -export type PatchToolVersion = { - isRecommended: boolean; - isDeprecated: boolean; +export type CreateToolNature = { + name: string; }; -export interface ToolNature { +export type ToolNature = { id: number; - name: string; -} +} & CreateToolNature; export type ToolExtended = { natures: ToolNature[]; @@ -78,14 +79,20 @@ export class ToolService { ); } - createTool(name: string): Observable { - return this.http.post(this.baseURL, { name }); + getToolByID(id: string): Observable { + return this.http.get(`${this.baseURL}/${id}`); + } + + getDefaultTool(): Observable { + return this.http.get(`${this.baseURL}/default`); + } + + createTool(tool: CreateTool): Observable { + return this.http.post(this.baseURL, tool); } - updateTool(toolId: number, toolName: string): Observable { - return this.http.put(`${this.baseURL}/${toolId}`, { - name: toolName, - }); + updateTool(toolId: number, value: Tool): Observable { + return this.http.put(`${this.baseURL}/${toolId}`, value); } deleteTool(tool_id: number): Observable { @@ -96,23 +103,28 @@ export class ToolService { return this.http.get(`${this.baseURL}/${toolId}/versions`); } - createVersionForTool(toolId: number, name: string): Observable { - return this.http.post(`${this.baseURL}/${toolId}/versions`, { - name, - }); + getDefaultVersion(): Observable { + return this.http.get(`${this.baseURL}/-/versions/default`); } - patchToolVersion( + createVersionForTool( + toolId: number, + toolVersion: CreateToolVersion, + ): Observable { + return this.http.post( + `${this.baseURL}/${toolId}/versions`, + toolVersion, + ); + } + + updateToolVersion( toolId: number, versionId: number, - updatedToolVersion: PatchToolVersion, + updatedToolVersion: CreateToolVersion, ) { - return this.http.patch( + return this.http.put( `${this.baseURL}/${toolId}/versions/${versionId}`, - { - is_recommended: updatedToolVersion.isRecommended, - is_deprecated: updatedToolVersion.isDeprecated, - }, + updatedToolVersion, ); } @@ -129,34 +141,37 @@ export class ToolService { return this.http.get(`${this.baseURL}/${toolId}/natures`); } - createNatureForTool(toolId: number, name: string): Observable { - return this.http.post(`${this.baseURL}/${toolId}/natures`, { - name, - }); + getDefaultNature(): Observable { + return this.http.get(`${this.baseURL}/-/natures/default`); } - deleteNatureForTool( + createNatureForTool( toolId: number, - toolNature: ToolNature, - ): Observable { - return this.http.delete( - `${this.baseURL}/${toolId}/natures/${toolNature.id}`, + toolNature: CreateToolNature, + ): Observable { + return this.http.post( + `${this.baseURL}/${toolId}/natures`, + toolNature, ); } - getDockerimagesForTool(toolId: number): Observable { - return this.http.get( - `${this.baseURL}/${toolId}/dockerimages`, + updateToolNature( + toolId: number, + natureID: number, + updatedToolNature: CreateToolNature, + ) { + return this.http.put( + `${this.baseURL}/${toolId}/natures/${natureID}`, + updatedToolNature, ); } - updateDockerimagesForTool( + deleteNatureForTool( toolId: number, - dockerimages: ToolDockerimages, - ): Observable { - return this.http.put( - `${this.baseURL}/${toolId}/dockerimages`, - dockerimages, + toolNature: ToolNature, + ): Observable { + return this.http.delete( + `${this.baseURL}/${toolId}/natures/${toolNature.id}`, ); } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 0cb32069e9..0ef1048099 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -21,7 +21,12 @@ module.exports = { button: "0.5rem", separator: "0.5rem", card: "400px", + "wide-card": "600px", "m-card": "1rem", + // The mat-cards should not overflow the screen + // 2*3.9px is the margin of the wrapper + // 2*10px is the margin of the mat-card + "max-card": "calc(100vw - 2*3.9px - 2*10px)", }, }, }, diff --git a/frontend/webpack.config.ts b/frontend/webpack.config.ts index 030166acb7..d2d567584d 100644 --- a/frontend/webpack.config.ts +++ b/frontend/webpack.config.ts @@ -12,7 +12,7 @@ export default (config: webpack.Configuration) => { languages: ['yaml'], }), ); - // Exclude monaco-editor existing css loader + // Exclude monaco-editor from existing Angular CSS loader const cssRuleIdx = config?.module?.rules?.findIndex( (rule: false | '' | 0 | webpack.RuleSetRule | '...' | null | undefined) => (rule as webpack.RuleSetRule).test?.toString().includes(':css'), @@ -25,11 +25,21 @@ export default (config: webpack.Configuration) => { { test: /\.css$/, include: /node_modules\/monaco-editor/, - use: ['style-loader', 'css-loader'], + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + // https://github.com/webpack/webpack-dev-server/issues/1815#issuecomment-1181720815 + url: false, + }, + }, + ], }, { test: /\.ttf$/, - use: ['file-loader'], + include: /node_modules\/monaco-editor/, + type: 'asset/resource', }, ); return config;