diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py index 56a310771..bcb71115e 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/crud.py @@ -6,7 +6,6 @@ import sqlalchemy as sa from sqlalchemy import orm -from capellacollab.core import database from capellacollab.projects.toolmodels import models as toolmodels_models from capellacollab.projects.toolmodels.modelsources.t4c import models from capellacollab.settings.modelsources.t4c.repositories import ( @@ -59,10 +58,12 @@ def create_t4c_model( def patch_t4c_model( db: orm.Session, t4c_model: models.DatabaseT4CModel, - patch_model: models.SubmitT4CModel, + repository: repositories_models.DatabaseT4CRepository, + name: str | None = None, ) -> models.DatabaseT4CModel: - database.patch_database_with_pydantic_object(t4c_model, patch_model) - + if name is not None: + t4c_model.name = name + t4c_model.repository = repository db.commit() return t4c_model diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/exceptions.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/exceptions.py index 5a2e21a08..cb6fc9bec 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/exceptions.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/exceptions.py @@ -47,3 +47,39 @@ def __init__(self): ), err_code="T4C_INTEGRATION_USED_IN_PIPELINES", ) + + +class T4CIntegrationWrongCapellaVersion(core_exceptions.BaseError): + def __init__( + self, + t4c_server_name: str, + t4c_repository_name: str, + server_version_name: str, + server_version_id: int, + model_version_name: str, + model_version_id: int, + ): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + title="The TeamForCapella server is not compatible with Capella model", + reason=( + f"The repository '{t4c_repository_name}' of the TeamForCapella server '{t4c_server_name}' " + f"has version {server_version_name} (ID {server_version_id}), " + f"but the model has version {model_version_name} (ID {model_version_id}). " + "Make sure that those versions match or are compatible with each other." + ), + err_code="T4C_INTEGRATION_WRONG_CAPELLA_VERSION", + ) + + +class T4CIntegrationVersionRequired(core_exceptions.BaseError): + def __init__(self, toolmodel_slug: str): + super().__init__( + status_code=status.HTTP_400_BAD_REQUEST, + title="The Capella model requires a version to proceed", + reason=( + f"To link a TeamForCapella repository, the Capella model '{toolmodel_slug}' has to have a version. " + "Please add a version first." + ), + err_code="T4C_INTEGRATION_NO_VERSION", + ) diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py index 2dbbd53db..b1713da78 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/models.py @@ -54,6 +54,12 @@ class SubmitT4CModel(core_pydantic.BaseModel): t4c_repository_id: int +class PatchT4CModel(core_pydantic.BaseModel): + name: str | None = None + t4c_instance_id: int | None = None + t4c_repository_id: int | None = None + + class SimpleT4CModel(core_pydantic.BaseModel): project_name: str repository_name: str diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py index 349d2c0ef..981f523fa 100644 --- a/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/routes.py @@ -15,14 +15,14 @@ from capellacollab.projects.toolmodels.backups import crud as backups_crud from capellacollab.projects.users import models as projects_users_models from capellacollab.settings.modelsources.t4c import ( - injectables as settings_t4c_injecatbles, + injectables as settings_t4c_injectables, ) from capellacollab.settings.modelsources.t4c.repositories import ( injectables as settings_t4c_repositories_injectables, ) from capellacollab.users import models as users_models -from . import crud, exceptions, injectables, models +from . import crud, exceptions, injectables, models, util router = fastapi.APIRouter( dependencies=[ @@ -87,7 +87,7 @@ def create_t4c_model( ), db: orm.Session = fastapi.Depends(database.get_db), ): - instance = settings_t4c_injecatbles.get_existing_unarchived_instance( + instance = settings_t4c_injectables.get_existing_unarchived_instance( body.t4c_instance_id, db ) repository = ( @@ -95,6 +95,11 @@ def create_t4c_model( body.t4c_repository_id, db, instance ) ) + + util.verify_compatibility_of_model_and_server( + model.name, model.version, repository + ) + try: return crud.create_t4c_model(db, model, repository, body.name) except exc.IntegrityError: @@ -118,14 +123,31 @@ def create_t4c_model( ], ), ) -def edit_t4c_model( - body: models.SubmitT4CModel, +def update_t4c_model( + body: models.PatchT4CModel, t4c_model: models.DatabaseT4CModel = fastapi.Depends( injectables.get_existing_t4c_model ), db: orm.Session = fastapi.Depends(database.get_db), ): - return crud.patch_t4c_model(db, t4c_model, body) + if body.t4c_instance_id is not None: + instance = settings_t4c_injectables.get_existing_unarchived_instance( + body.t4c_instance_id, db + ) + else: + instance = t4c_model.repository.instance + + repository = ( + settings_t4c_repositories_injectables.get_existing_t4c_repository( + body.t4c_repository_id or t4c_model.repository.id, db, instance + ) + ) + + util.verify_compatibility_of_model_and_server( + t4c_model.model.name, t4c_model.model.version, repository + ) + + return crud.patch_t4c_model(db, t4c_model, repository, body.name) @router.delete( diff --git a/backend/capellacollab/projects/toolmodels/modelsources/t4c/util.py b/backend/capellacollab/projects/toolmodels/modelsources/t4c/util.py new file mode 100644 index 000000000..d78af86c3 --- /dev/null +++ b/backend/capellacollab/projects/toolmodels/modelsources/t4c/util.py @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +from capellacollab.settings.modelsources.t4c.repositories import ( + models as t4c_repository_models, +) +from capellacollab.tools import models as tools_models + +from . import exceptions + + +def verify_compatibility_of_model_and_server( + model_name: str, + model_version: tools_models.DatabaseVersion | None, + t4c_repository: t4c_repository_models.DatabaseT4CRepository, +): + server = t4c_repository.instance + if model_version is None: + raise exceptions.T4CIntegrationVersionRequired(model_name) + + if ( + t4c_repository.instance.version.id + not in model_version.config.compatible_versions + [model_version.id] + ): + raise exceptions.T4CIntegrationWrongCapellaVersion( + server.name, + t4c_repository.name, + server.version.name, + server.version.id, + model_version.name, + model_version.id, + ) diff --git a/backend/capellacollab/projects/toolmodels/routes.py b/backend/capellacollab/projects/toolmodels/routes.py index 325c509ec..61b7817f3 100644 --- a/backend/capellacollab/projects/toolmodels/routes.py +++ b/backend/capellacollab/projects/toolmodels/routes.py @@ -5,13 +5,14 @@ import fastapi import slugify -from sqlalchemy import exc, orm +from sqlalchemy import orm from capellacollab.core import database from capellacollab.core import exceptions as core_exceptions from capellacollab.core.authentication import injectables as auth_injectables from capellacollab.projects import injectables as projects_injectables from capellacollab.projects import models as projects_models +from capellacollab.projects.toolmodels.modelsources.t4c import util as t4c_util from capellacollab.projects.users import models as projects_users_models from capellacollab.tools import injectables as tools_injectables from capellacollab.users import injectables as users_injectables @@ -85,14 +86,13 @@ def create_new_tool_model( if tool.integrations.jupyter: configuration["workspace"] = str(uuid.uuid4()) - try: - model = crud.create_model( - db, project, new_model, tool, configuration=configuration - ) - except exc.IntegrityError: - raise exceptions.ToolModelAlreadyExistsError( - project.slug, slugify.slugify(new_model.name) - ) + slug = slugify.slugify(new_model.name) + if crud.get_model_by_slugs(db, project.slug, slug): + raise exceptions.ToolModelAlreadyExistsError(project.slug, slug) + + model = crud.create_model( + db, project, new_model, tool, configuration=configuration + ) if tool.integrations.jupyter: workspace.create_shared_workspace( @@ -153,6 +153,11 @@ def patch_tool_model( else model.nature ) + for t4c_model in model.t4c_models: + t4c_util.verify_compatibility_of_model_and_server( + model.name, version, t4c_model.repository + ) + if body.project_slug: new_project = determine_new_project_to_move_model( body.project_slug, db, user diff --git a/backend/capellacollab/settings/modelsources/t4c/models.py b/backend/capellacollab/settings/modelsources/t4c/models.py index 0d1294257..478a8b2d9 100644 --- a/backend/capellacollab/settings/modelsources/t4c/models.py +++ b/backend/capellacollab/settings/modelsources/t4c/models.py @@ -129,7 +129,6 @@ class PatchT4CInstance(core_pydantic.BaseModel): username: str | None = None password: str | None = None protocol: Protocol | None = None - version_id: int | None = None is_archived: bool | None = None _validate_rest_api_url = pydantic.field_validator("rest_api")( diff --git a/backend/tests/projects/toolmodels/fixtures.py b/backend/tests/projects/toolmodels/fixtures.py index ebfeeba84..84f9f4e5f 100644 --- a/backend/tests/projects/toolmodels/fixtures.py +++ b/backend/tests/projects/toolmodels/fixtures.py @@ -7,11 +7,6 @@ import capellacollab.projects.models as projects_models import capellacollab.projects.toolmodels.crud as toolmodels_crud import capellacollab.projects.toolmodels.models as toolmodels_models -import capellacollab.projects.toolmodels.modelsources.git.crud as project_git_crud -import capellacollab.projects.toolmodels.modelsources.git.models as project_git_models -import capellacollab.projects.toolmodels.modelsources.t4c.crud as models_t4c_crud -import capellacollab.projects.toolmodels.modelsources.t4c.models as models_t4c_models -import capellacollab.settings.modelsources.t4c.repositories.models as settings_t4c_repositories_models import capellacollab.tools.models as tools_models @@ -45,30 +40,3 @@ def fixture_jupyter_model( tool=jupyter_tool, configuration={"workspace": "test"}, ) - - -@pytest.fixture(name="git_model") -def fixture_git_model( - db: orm.Session, capella_model: toolmodels_models.DatabaseToolModel -) -> project_git_models.DatabaseGitModel: - git_model = project_git_models.PostGitModel( - path="https://example.com/test/project", - entrypoint="test/test.aird", - revision="main", - username="user", - password="password", - ) - return project_git_crud.add_git_model_to_capellamodel( - db, capella_model, git_model - ) - - -@pytest.fixture(name="t4c_model") -def fixture_t4c_model( - db: orm.Session, - capella_model: toolmodels_models.DatabaseToolModel, - t4c_repository: settings_t4c_repositories_models.DatabaseT4CRepository, -) -> models_t4c_models.DatabaseT4CModel: - return models_t4c_crud.create_t4c_model( - db, capella_model, t4c_repository, "default" - ) diff --git a/backend/tests/projects/toolmodels/modelsources/fixtures.py b/backend/tests/projects/toolmodels/modelsources/fixtures.py new file mode 100644 index 000000000..4d1299d03 --- /dev/null +++ b/backend/tests/projects/toolmodels/modelsources/fixtures.py @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + +import pytest +from sqlalchemy import orm + +import capellacollab.projects.toolmodels.models as toolmodels_models +import capellacollab.projects.toolmodels.modelsources.git.crud as project_git_crud +import capellacollab.projects.toolmodels.modelsources.git.models as project_git_models +import capellacollab.projects.toolmodels.modelsources.t4c.crud as models_t4c_crud +import capellacollab.projects.toolmodels.modelsources.t4c.models as models_t4c_models +import capellacollab.settings.modelsources.t4c.repositories.models as settings_t4c_repositories_models + + +@pytest.fixture(name="t4c_model") +def fixture_t4c_model( + db: orm.Session, + capella_model: toolmodels_models.DatabaseToolModel, + t4c_repository: settings_t4c_repositories_models.DatabaseT4CRepository, +) -> models_t4c_models.DatabaseT4CModel: + return models_t4c_crud.create_t4c_model( + db, capella_model, t4c_repository, "default" + ) + + +@pytest.fixture(name="git_model") +def fixture_git_model( + db: orm.Session, capella_model: toolmodels_models.DatabaseToolModel +) -> project_git_models.DatabaseGitModel: + git_model = project_git_models.PostGitModel( + path="https://example.com/test/project", + entrypoint="test/test.aird", + revision="main", + username="user", + password="password", + ) + return project_git_crud.add_git_model_to_capellamodel( + db, capella_model, git_model + ) diff --git a/backend/tests/projects/toolmodels/modelsources/test_t4c_routes.py b/backend/tests/projects/toolmodels/modelsources/test_t4c_routes.py new file mode 100644 index 000000000..bf3d0b17d --- /dev/null +++ b/backend/tests/projects/toolmodels/modelsources/test_t4c_routes.py @@ -0,0 +1,252 @@ +# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors +# SPDX-License-Identifier: Apache-2.0 + + +import pytest +from fastapi import testclient +from sqlalchemy import orm + +import capellacollab.projects.models as projects_models +import capellacollab.settings.modelsources.t4c.repositories.crud as t4c_repositories_crud +import capellacollab.settings.modelsources.t4c.repositories.models as t4c_repositories_models +from capellacollab.projects.toolmodels import crud as toolmodels_crud +from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.projects.toolmodels.modelsources.t4c import crud as t4c_crud +from capellacollab.projects.toolmodels.modelsources.t4c import ( + models as t4c_models, +) +from capellacollab.settings.modelsources.t4c import crud as t4c_servers_crud +from capellacollab.settings.modelsources.t4c import ( + models as t4c_servers_models, +) +from capellacollab.settings.modelsources.t4c.repositories import ( + models as t4c_repositories_models, +) +from capellacollab.tools import models as tools_models + + +@pytest.mark.usefixtures("project_manager") +def test_list_t4c_models( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + t4c_model: t4c_models.DatabaseT4CModel, +): + response = client.get( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c" + ) + + assert response.status_code == 200 + assert len(response.json()) == 1 + assert response.json()[0]["name"] == t4c_model.name + + +@pytest.mark.usefixtures("project_manager") +def test_get_t4c_model( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + t4c_model: t4c_models.DatabaseT4CModel, +): + response = client.get( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c/{t4c_model.id}" + ) + + assert response.status_code == 200 + assert response.json()["name"] == t4c_model.name + assert ( + response.json()["repository"]["instance"]["id"] + == t4c_model.repository.instance.id + ) + + +@pytest.mark.usefixtures("admin") +def test_create_t4c_model( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + t4c_repository: t4c_repositories_models.DatabaseT4CRepository, + capella_model: toolmodels_models.DatabaseToolModel, +): + response = client.post( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c", + json={ + "name": "project", + "t4c_instance_id": t4c_repository.instance.id, + "t4c_repository_id": t4c_repository.id, + }, + ) + + assert response.status_code == 200 + assert response.json()["name"] == "project" + assert response.json()["repository"]["id"] == t4c_repository.id + assert ( + response.json()["repository"]["instance"]["id"] + == t4c_repository.instance.id + ) + + +@pytest.mark.usefixtures("admin", "t4c_model") +def test_create_t4c_model_twice_fails( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + t4c_repository: t4c_repositories_models.DatabaseT4CRepository, + capella_model: toolmodels_models.DatabaseToolModel, +): + response = client.post( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c", + json={ + "name": "default", + "t4c_instance_id": t4c_repository.instance.id, + "t4c_repository_id": t4c_repository.id, + }, + ) + + assert response.status_code == 409 + assert ( + response.json()["detail"]["err_code"] + == "T4C_INTEGRATION_ALREADY_EXISTS" + ) + + +@pytest.mark.usefixtures("admin") +def test_create_t4c_model_incompatible_version( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + t4c_repository: t4c_repositories_models.DatabaseT4CRepository, + capella_tool: tools_models.DatabaseTool, + db: orm.Session, + tool_version: tools_models.DatabaseVersion, +): + model = toolmodels_crud.create_model( + db, + project, + toolmodels_models.PostToolModel( + name="test", description="test", tool_id=capella_tool.id + ), + capella_tool, + tool_version, + ) + response = client.post( + f"/api/v1/projects/{project.slug}/models/{model.slug}/modelsources/t4c", + json={ + "name": "project", + "t4c_instance_id": t4c_repository.instance.id, + "t4c_repository_id": t4c_repository.id, + }, + ) + + assert response.status_code == 400 + assert ( + response.json()["detail"]["err_code"] + == "T4C_INTEGRATION_WRONG_CAPELLA_VERSION" + ) + + +@pytest.mark.usefixtures("admin") +def test_create_t4c_model_without_version( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + t4c_repository: t4c_repositories_models.DatabaseT4CRepository, + db: orm.Session, + capella_tool: tools_models.DatabaseTool, +): + model = toolmodels_crud.create_model( + db, + project, + toolmodels_models.PostToolModel( + name="test", description="test", tool_id=capella_tool.id + ), + capella_tool, + ) + response = client.post( + f"/api/v1/projects/{project.slug}/models/{model.slug}/modelsources/t4c", + json={ + "name": "project", + "t4c_instance_id": t4c_repository.instance.id, + "t4c_repository_id": t4c_repository.id, + }, + ) + + assert response.status_code == 400 + assert ( + response.json()["detail"]["err_code"] == "T4C_INTEGRATION_NO_VERSION" + ) + + +@pytest.mark.usefixtures("admin") +def test_change_server_of_t4c_model( + db: orm.Session, + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + t4c_model: t4c_models.DatabaseT4CModel, + capella_tool_version: tools_models.DatabaseVersion, +): + server = t4c_servers_models.DatabaseT4CInstance( + name="test server 2", + license="lic", + host="localhost", + usage_api="http://localhost:8086", + rest_api="http://localhost:8080/api/v1.0", + username="user", + password="pass", + protocol=t4c_servers_models.Protocol.tcp, + version=capella_tool_version, + ) + db_server = t4c_servers_crud.create_t4c_instance(db, server) + + second_t4c_repository = t4c_repositories_crud.create_t4c_repository( + db=db, repo_name="test2", instance=db_server + ) + + response = client.patch( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c/{t4c_model.id}", + json={ + "t4c_instance_id": db_server.id, + "t4c_repository_id": second_t4c_repository.id, + }, + ) + + assert response.status_code == 200 + assert response.json()["name"] == t4c_model.name + assert response.json()["repository"]["id"] == second_t4c_repository.id + assert ( + response.json()["repository"]["instance"]["id"] + == second_t4c_repository.instance.id + ) + + +@pytest.mark.usefixtures("admin") +def test_patch_name_of_t4c_model( + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + t4c_model: t4c_models.DatabaseT4CModel, +): + response = client.patch( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c/{t4c_model.id}", + json={"name": "new_default"}, + ) + + assert response.status_code == 200 + assert response.json()["name"] == "new_default" + assert ( + response.json()["repository"]["instance"]["id"] + == t4c_model.repository.instance.id + ) + + +@pytest.mark.usefixtures("admin") +def test_unlink_t4c_model( + db: orm.Session, + client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + t4c_model: t4c_models.DatabaseT4CModel, +): + response = client.delete( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}/modelsources/t4c/{t4c_model.id}", + ) + + assert response.status_code == 204 + assert t4c_crud.get_t4c_model_by_id(db, t4c_model.id) is None diff --git a/backend/tests/projects/toolmodels/test_toolmodel_routes.py b/backend/tests/projects/toolmodels/test_toolmodel_routes.py index 9ed96f258..ee3d8c8a0 100644 --- a/backend/tests/projects/toolmodels/test_toolmodel_routes.py +++ b/backend/tests/projects/toolmodels/test_toolmodel_routes.py @@ -2,46 +2,23 @@ # SPDX-License-Identifier: Apache-2.0 -from unittest import mock +from uuid import uuid4 import pytest from fastapi import testclient from sqlalchemy import orm -from capellacollab.__main__ import app -from capellacollab.projects import injectables as projects_injectables +import capellacollab.projects.users.crud as projects_users_crud +import capellacollab.projects.users.models as projects_users_models +from capellacollab.projects import crud as projects_crud from capellacollab.projects import models as projects_models -from capellacollab.projects.toolmodels import ( - injectables as toolmodels_injectables, -) from capellacollab.projects.toolmodels import models as toolmodels_models +from capellacollab.tools import crud as tools_crud +from capellacollab.tools import models as tools_models from capellacollab.users import crud as users_crud from capellacollab.users import models as users_models -@pytest.fixture(name="override_dependency") -def fixture_override_dependency(): - mock_project = mock.Mock(name="DatabaseProject") - - mock_model = mock.Mock(name="DatabaseModel") - mock_model.slug = "any-slug" - mock_model.tool = mock.Mock(name="tool") - - app.dependency_overrides[projects_injectables.get_existing_project] = ( - lambda: mock_project - ) - app.dependency_overrides[ - toolmodels_injectables.get_existing_capella_model - ] = lambda: mock_model - - yield - - del app.dependency_overrides[projects_injectables.get_existing_project] - del app.dependency_overrides[ - toolmodels_injectables.get_existing_capella_model - ] - - def test_rename_toolmodel_successful( capella_model: toolmodels_models.DatabaseToolModel, project: projects_models.DatabaseProject, @@ -66,9 +43,11 @@ def test_rename_toolmodel_successful( assert "new-name" in response.text -@pytest.mark.usefixtures("override_dependency") def test_rename_toolmodel_where_name_already_exists( client: testclient.TestClient, + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.DatabaseToolModel, + jupyter_model: toolmodels_models.DatabaseToolModel, executor_name: str, db: orm.Session, ): @@ -76,22 +55,13 @@ def test_rename_toolmodel_where_name_already_exists( db, executor_name, executor_name, None, users_models.Role.ADMIN ) - with mock.patch( - "capellacollab.projects.toolmodels.crud.get_model_by_slugs", - autospec=True, - ) as mock_get_model_by_slugs: - mock_get_model_by_slugs.return_value = "anything" - - response = client.patch( - "/api/v1/projects/any/models/any", - json={"name": "new-name", "version_id": -1, "nature_id": -1}, - ) + response = client.patch( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}", + json={"name": jupyter_model.name, "version_id": -1, "nature_id": -1}, + ) - assert response.status_code == 409 - assert ( - response.json()["detail"]["err_code"] == "TOOLMODEL_ALREADY_EXISTS" - ) - mock_get_model_by_slugs.assert_called_once() + assert response.status_code == 409 + assert response.json()["detail"]["err_code"] == "TOOLMODEL_ALREADY_EXISTS" def test_update_toolmodel_order_successful( @@ -112,3 +82,76 @@ def test_update_toolmodel_order_successful( assert response.status_code == 200 assert "1" in response.text + + +def test_move_toolmodel( + project: projects_models.DatabaseProject, + project_manager: users_models.DatabaseUser, + capella_model: toolmodels_models.ToolModel, + client: testclient.TestClient, + db: orm.Session, +): + second_project = projects_crud.create_project(db, str(uuid4())) + projects_users_crud.add_user_to_project( + db, + project=second_project, + user=project_manager, + role=projects_users_models.ProjectUserRole.MANAGER, + permission=projects_users_models.ProjectUserPermission.WRITE, + ) + + response = client.patch( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}", + json={"project_slug": second_project.slug}, + ) + assert response.status_code == 200 + + response = client.get( + f"/api/v1/projects/{second_project.slug}/models/{capella_model.slug}" + ) + assert response.status_code == 200 + + +@pytest.mark.usefixtures("project_manager") +def test_move_toolmodel_non_project_member( + project: projects_models.DatabaseProject, + capella_model: toolmodels_models.ToolModel, + client: testclient.TestClient, + db: orm.Session, +): + second_project = projects_crud.create_project(db, str(uuid4())) + + response = client.patch( + f"/api/v1/projects/{project.slug}/models/{capella_model.slug}", + json={"project_slug": second_project.slug}, + ) + assert response.status_code == 403 + + +@pytest.mark.usefixtures("t4c_model", "project_manager") +def test_patch_toolmodel_version_with_invalid_t4c_link( + db: orm.Session, + client: testclient.TestClient, + capella_model: toolmodels_models.DatabaseToolModel, + capella_tool: tools_models.DatabaseTool, +): + version = tools_crud.create_version( + db, + capella_tool, + tools_models.CreateToolVersion( + name="1.0.0", config=tools_models.ToolVersionConfiguration() + ), + ) + + response = client.patch( + f"/api/v1/projects/{capella_model.project.slug}/models/{capella_model.slug}", + json={ + "version_id": version.id, + }, + ) + + assert response.status_code == 400 + assert ( + response.json()["detail"]["err_code"] + == "T4C_INTEGRATION_WRONG_CAPELLA_VERSION" + ) diff --git a/backend/tests/projects/toolmodels/test_toolmodels.py b/backend/tests/projects/toolmodels/test_toolmodels.py deleted file mode 100644 index 6b0850b58..000000000 --- a/backend/tests/projects/toolmodels/test_toolmodels.py +++ /dev/null @@ -1,59 +0,0 @@ -# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors -# SPDX-License-Identifier: Apache-2.0 - -from uuid import uuid4 - -import pytest -from fastapi import testclient -from sqlalchemy import orm - -import capellacollab.projects.users.crud as projects_users_crud -import capellacollab.projects.users.models as projects_users_models -from capellacollab.projects import crud as projects_crud -from capellacollab.projects import models as projects_models -from capellacollab.projects.toolmodels import models as toolmodels_models -from capellacollab.users import models as users_models - - -def test_move_toolmodel( - project: projects_models.DatabaseProject, - project_manager: users_models.DatabaseUser, - capella_model: toolmodels_models.ToolModel, - client: testclient.TestClient, - db: orm.Session, -): - second_project = projects_crud.create_project(db, str(uuid4())) - projects_users_crud.add_user_to_project( - db, - project=second_project, - user=project_manager, - role=projects_users_models.ProjectUserRole.MANAGER, - permission=projects_users_models.ProjectUserPermission.WRITE, - ) - - response = client.patch( - f"/api/v1/projects/{project.slug}/models/{capella_model.slug}", - json={"project_slug": second_project.slug}, - ) - assert response.status_code == 200 - - response = client.get( - f"/api/v1/projects/{second_project.slug}/models/{capella_model.slug}" - ) - assert response.status_code == 200 - - -@pytest.mark.usefixtures("project_manager") -def test_move_toolmodel_non_project_member( - project: projects_models.DatabaseProject, - capella_model: toolmodels_models.ToolModel, - client: testclient.TestClient, - db: orm.Session, -): - second_project = projects_crud.create_project(db, str(uuid4())) - - response = client.patch( - f"/api/v1/projects/{project.slug}/models/{capella_model.slug}", - json={"project_slug": second_project.slug}, - ) - assert response.status_code == 403 diff --git a/backend/tests/settings/teamforcapella/fixtures.py b/backend/tests/settings/teamforcapella/fixtures.py index eef937409..4033c48b7 100644 --- a/backend/tests/settings/teamforcapella/fixtures.py +++ b/backend/tests/settings/teamforcapella/fixtures.py @@ -14,7 +14,7 @@ @pytest.fixture(name="t4c_instance") def fixture_t4c_instance( db: orm.Session, - tool_version: tools_models.DatabaseVersion, + capella_tool_version: tools_models.DatabaseVersion, ) -> t4c_models.DatabaseT4CInstance: server = t4c_models.DatabaseT4CInstance( name="test server", @@ -25,7 +25,7 @@ def fixture_t4c_instance( username="user", password="pass", protocol=t4c_models.Protocol.tcp, - version=tool_version, + version=capella_tool_version, ) return t4c_crud.create_t4c_instance(db, server) diff --git a/backend/tests/tools/fixtures.py b/backend/tests/tools/fixtures.py index 452db8eba..711617ffb 100644 --- a/backend/tests/tools/fixtures.py +++ b/backend/tests/tools/fixtures.py @@ -60,20 +60,6 @@ def fixture_capella_tool(db: orm.Session) -> tools_models.DatabaseTool: return capella_tool -@pytest.fixture(name="capella_tool_version", params=["6.0.0"]) -def fixture_capella_tool_version( - db: orm.Session, - capella_tool: tools_models.DatabaseTool, - request: pytest.FixtureRequest, -) -> tools_models.DatabaseVersion: - capella_tool_version = tools_crud.get_version_by_tool_id_version_name( - db, capella_tool.id, request.param - ) - assert capella_tool_version - - return capella_tool_version - - @pytest.fixture(name="jupyter_tool") def fixture_jupyter_tool(db: orm.Session) -> tools_models.DatabaseTool: return database_migration.create_jupyter_tool(db) diff --git a/backend/tests/tools/versions/fixtures.py b/backend/tests/tools/versions/fixtures.py index cd8ac96af..254098500 100644 --- a/backend/tests/tools/versions/fixtures.py +++ b/backend/tests/tools/versions/fixtures.py @@ -9,6 +9,20 @@ from capellacollab.tools import models as tools_models +@pytest.fixture(name="capella_tool_version", params=["6.0.0"]) +def fixture_capella_tool_version( + db: orm.Session, + capella_tool: tools_models.DatabaseTool, + request: pytest.FixtureRequest, +) -> tools_models.DatabaseVersion: + capella_tool_version = tools_crud.get_version_by_tool_id_version_name( + db, capella_tool.id, request.param + ) + assert capella_tool_version + + return capella_tool_version + + @pytest.fixture(name="tool_version") def fixture_tool_version( db: orm.Session, diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 250ae5c26..f647c55e4 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -145,7 +145,91 @@ export const routes: Routes = [ redirect: (data: Data) => `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources`, }, - component: ModelDetailComponent, + children: [ + { + path: '', + data: { + breadcrumb: undefined, + }, + component: ModelDetailComponent, + }, + { + path: 'git-model', + data: { + breadcrumb: 'Git Repositories', + }, + children: [ + { + path: 'create', + data: { + breadcrumb: 'Add', + redirect: (data: Data) => + `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources/git-model/create`, + }, + component: ManageGitModelComponent, + }, + { + path: ':git-model', + data: { + breadcrumb: (data: Data) => + data.gitModel?.id || '...', + redirect: (data: Data) => + data.gitModel?.id + ? `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources/git-model/${data.gitModel?.id}` + : undefined, + }, + component: ManageGitModelComponent, + }, + ], + }, + { + path: 't4c-model', + data: { + breadcrumb: 'T4C Repositories', + }, + children: [ + { + path: 'create-existing', + data: { + breadcrumb: 'Link Existing Repository', + redirect: (data: Data) => + `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources/t4c-model/create-existing`, + }, + component: ManageT4CModelComponent, + }, + { + path: 'create-new', + data: { + breadcrumb: 'Create New Repository', + redirect: (data: Data) => + `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources/t4c-model/create-new`, + }, + component: CreateT4cModelNewRepositoryComponent, + }, + { + path: ':t4c_model_id', + data: { + breadcrumb: (data: Data) => + data.t4cModel?.id || '...', + redirect: (data: Data) => + data.t4cModel?.id + ? `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources/t4c-model/${data.t4cModel?.id}` + : undefined, + }, + component: T4CModelWrapperComponent, + children: [ + { + path: '', + data: { + breadcrumb: undefined, + }, + component: ManageT4CModelComponent, + }, + ], + }, + ], + }, + ], }, { path: 'metadata', @@ -220,83 +304,6 @@ export const routes: Routes = [ }, ], }, - { - path: 'git-model', - data: { - breadcrumb: 'Model Sources', - redirect: (data: Data) => - `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources`, - }, - children: [ - { - path: 'create', - data: { - breadcrumb: 'Create Git Model', - redirect: (data: Data) => - `/project/${data.project?.slug}/model/${data.model?.slug}/git-model/create`, - }, - component: ManageGitModelComponent, - }, - { - path: ':git-model', - data: { - breadcrumb: (data: Data) => - data.gitModel?.id || '...', - redirect: (data: Data) => - data.gitModel?.id - ? `/project/${data.project?.slug}/model/${data.model?.slug}/git-model/${data.gitModel?.id}` - : undefined, - }, - component: ManageGitModelComponent, - }, - ], - }, - { - path: 't4c-model', - data: { - breadcrumb: 'Model Sources', - redirect: (data: Data) => - `/project/${data.project?.slug}/model/${data.model?.slug}/modelsources`, - }, - children: [ - { - path: 'create-existing', - data: { - breadcrumb: 'Create T4C Model', - redirect: (data: Data) => - `/project/${data.project?.slug}/model/${data.model?.slug}/t4c-model/create-existing`, - }, - component: ManageT4CModelComponent, - }, - { - path: 'create-new', - data: { - breadcrumb: 'Create T4C Model', - redirect: (data: Data) => - `/project/${data.project?.slug}/model/${data.model?.slug}/t4c-model/create-new`, - }, - component: CreateT4cModelNewRepositoryComponent, - }, - { - path: ':t4c_model_id', - data: { - breadcrumb: (data: Data) => - data.t4cModel?.id || '...', - redirect: (data: Data) => - data.t4cModel?.id - ? `/project/${data.project?.slug}/model/${data.model?.slug}/t4c-model/${data.t4cModel?.id}` - : undefined, - }, - component: T4CModelWrapperComponent, - children: [ - { - path: '', - component: ManageT4CModelComponent, - }, - ], - }, - ], - }, ], }, ], diff --git a/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.css b/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.css deleted file mode 100644 index cb13f1cd1..000000000 --- a/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.css +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -::ng-deep .skeleton-loader.circle { - margin: 0px !important; -} - -.margin { - margin-bottom: 18.813px; -} diff --git a/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.html b/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.html index cbf5ff951..8ba9d65e9 100644 --- a/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.html +++ b/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.html @@ -2,13 +2,14 @@ ~ SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors ~ SPDX-License-Identifier: Apache-2.0 --> -
+
diff --git a/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.ts b/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.ts index fa763fc08..6891ced67 100644 --- a/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.ts +++ b/frontend/src/app/helpers/skeleton-loaders/form-field-skeleton-loader/form-field-skeleton-loader.component.ts @@ -8,7 +8,6 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @Component({ selector: 'app-form-field-skeleton-loader', templateUrl: './form-field-skeleton-loader.component.html', - styleUrls: ['./form-field-skeleton-loader.component.css'], standalone: true, imports: [NgxSkeletonLoaderModule], }) diff --git a/frontend/src/app/openapi/.openapi-generator/FILES b/frontend/src/app/openapi/.openapi-generator/FILES index e2b8c8111..9531e2e7f 100644 --- a/frontend/src/app/openapi/.openapi-generator/FILES +++ b/frontend/src/app/openapi/.openapi-generator/FILES @@ -101,6 +101,7 @@ model/page-pipeline-run.ts model/patch-project-user.ts model/patch-project.ts model/patch-t4-c-instance.ts +model/patch-t4-c-model.ts model/patch-tool-model.ts model/patch-user.ts model/path-validation.ts diff --git a/frontend/src/app/openapi/api/projects-models-t4-c.service.ts b/frontend/src/app/openapi/api/projects-models-t4-c.service.ts index 5a6dc8a2d..0704a8a25 100644 --- a/frontend/src/app/openapi/api/projects-models-t4-c.service.ts +++ b/frontend/src/app/openapi/api/projects-models-t4-c.service.ts @@ -21,6 +21,8 @@ import { Observable } from 'rxjs'; // @ts-ignore import { HTTPValidationError } from '../model/http-validation-error'; // @ts-ignore +import { PatchT4CModel } from '../model/patch-t4-c-model'; +// @ts-ignore import { SubmitT4CModel } from '../model/submit-t4-c-model'; // @ts-ignore import { T4CModel } from '../model/t4-c-model'; @@ -265,29 +267,25 @@ export class ProjectsModelsT4CService { } /** - * Edit T4C Model + * Get T4C Model * @param projectSlug * @param t4cModelId * @param modelSlug - * @param submitT4CModel * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public editT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, submitT4CModel: SubmitT4CModel, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public editT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, submitT4CModel: SubmitT4CModel, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public editT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, submitT4CModel: SubmitT4CModel, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public editT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, submitT4CModel: SubmitT4CModel, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (projectSlug === null || projectSlug === undefined) { - throw new Error('Required parameter projectSlug was null or undefined when calling editT4cModel.'); + throw new Error('Required parameter projectSlug was null or undefined when calling getT4cModel.'); } if (t4cModelId === null || t4cModelId === undefined) { - throw new Error('Required parameter t4cModelId was null or undefined when calling editT4cModel.'); + throw new Error('Required parameter t4cModelId was null or undefined when calling getT4cModel.'); } if (modelSlug === null || modelSlug === undefined) { - throw new Error('Required parameter modelSlug was null or undefined when calling editT4cModel.'); - } - if (submitT4CModel === null || submitT4CModel === undefined) { - throw new Error('Required parameter submitT4CModel was null or undefined when calling editT4cModel.'); + throw new Error('Required parameter modelSlug was null or undefined when calling getT4cModel.'); } let localVarHeaders = this.defaultHeaders; @@ -322,15 +320,6 @@ export class ProjectsModelsT4CService { } - // to determine the Content-Type header - const consumes: string[] = [ - 'application/json' - ]; - const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); - if (httpContentTypeSelected !== undefined) { - localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); - } - let responseType_: 'text' | 'json' | 'blob' = 'json'; if (localVarHttpHeaderAcceptSelected) { if (localVarHttpHeaderAcceptSelected.startsWith('text')) { @@ -343,10 +332,9 @@ export class ProjectsModelsT4CService { } let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/modelsources/t4c/${this.configuration.encodeParam({name: "t4cModelId", value: t4cModelId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; - return this.httpClient.request('patch', `${this.configuration.basePath}${localVarPath}`, + return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, - body: submitT4CModel, responseType: responseType_, withCredentials: this.configuration.withCredentials, headers: localVarHeaders, @@ -358,25 +346,21 @@ export class ProjectsModelsT4CService { } /** - * Get T4C Model + * List T4C Models * @param projectSlug - * @param t4cModelId * @param modelSlug * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; - public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public getT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + public listT4cModels(projectSlug: string, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public listT4cModels(projectSlug: string, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public listT4cModels(projectSlug: string, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; + public listT4cModels(projectSlug: string, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (projectSlug === null || projectSlug === undefined) { - throw new Error('Required parameter projectSlug was null or undefined when calling getT4cModel.'); - } - if (t4cModelId === null || t4cModelId === undefined) { - throw new Error('Required parameter t4cModelId was null or undefined when calling getT4cModel.'); + throw new Error('Required parameter projectSlug was null or undefined when calling listT4cModels.'); } if (modelSlug === null || modelSlug === undefined) { - throw new Error('Required parameter modelSlug was null or undefined when calling getT4cModel.'); + throw new Error('Required parameter modelSlug was null or undefined when calling listT4cModels.'); } let localVarHeaders = this.defaultHeaders; @@ -422,8 +406,8 @@ export class ProjectsModelsT4CService { } } - let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/modelsources/t4c/${this.configuration.encodeParam({name: "t4cModelId", value: t4cModelId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; - return this.httpClient.request('get', `${this.configuration.basePath}${localVarPath}`, + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/modelsources/t4c`; + return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, responseType: responseType_, @@ -437,21 +421,29 @@ export class ProjectsModelsT4CService { } /** - * List T4C Models + * Update T4C Model * @param projectSlug + * @param t4cModelId * @param modelSlug + * @param patchT4CModel * @param observe set whether or not to return the data Observable as the body, response or events. defaults to returning the body. * @param reportProgress flag to report request and response progress. */ - public listT4cModels(projectSlug: string, modelSlug: string, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; - public listT4cModels(projectSlug: string, modelSlug: string, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public listT4cModels(projectSlug: string, modelSlug: string, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>>; - public listT4cModels(projectSlug: string, modelSlug: string, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + public updateT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, patchT4CModel: PatchT4CModel, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public updateT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, patchT4CModel: PatchT4CModel, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, patchT4CModel: PatchT4CModel, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public updateT4cModel(projectSlug: string, t4cModelId: number, modelSlug: string, patchT4CModel: PatchT4CModel, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { if (projectSlug === null || projectSlug === undefined) { - throw new Error('Required parameter projectSlug was null or undefined when calling listT4cModels.'); + throw new Error('Required parameter projectSlug was null or undefined when calling updateT4cModel.'); + } + if (t4cModelId === null || t4cModelId === undefined) { + throw new Error('Required parameter t4cModelId was null or undefined when calling updateT4cModel.'); } if (modelSlug === null || modelSlug === undefined) { - throw new Error('Required parameter modelSlug was null or undefined when calling listT4cModels.'); + throw new Error('Required parameter modelSlug was null or undefined when calling updateT4cModel.'); + } + if (patchT4CModel === null || patchT4CModel === undefined) { + throw new Error('Required parameter patchT4CModel was null or undefined when calling updateT4cModel.'); } let localVarHeaders = this.defaultHeaders; @@ -486,6 +478,15 @@ export class ProjectsModelsT4CService { } + // to determine the Content-Type header + const consumes: string[] = [ + 'application/json' + ]; + const httpContentTypeSelected: string | undefined = this.configuration.selectHeaderContentType(consumes); + if (httpContentTypeSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Content-Type', httpContentTypeSelected); + } + let responseType_: 'text' | 'json' | 'blob' = 'json'; if (localVarHttpHeaderAcceptSelected) { if (localVarHttpHeaderAcceptSelected.startsWith('text')) { @@ -497,10 +498,11 @@ export class ProjectsModelsT4CService { } } - let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/modelsources/t4c`; - return this.httpClient.request>('get', `${this.configuration.basePath}${localVarPath}`, + let localVarPath = `/api/v1/projects/${this.configuration.encodeParam({name: "projectSlug", value: projectSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/models/${this.configuration.encodeParam({name: "modelSlug", value: modelSlug, in: "path", style: "simple", explode: false, dataType: "string", dataFormat: undefined})}/modelsources/t4c/${this.configuration.encodeParam({name: "t4cModelId", value: t4cModelId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; + return this.httpClient.request('patch', `${this.configuration.basePath}${localVarPath}`, { context: localVarHttpContext, + body: patchT4CModel, responseType: responseType_, withCredentials: this.configuration.withCredentials, headers: localVarHeaders, diff --git a/frontend/src/app/openapi/model/models.ts b/frontend/src/app/openapi/model/models.ts index 547f2b1aa..57d767148 100644 --- a/frontend/src/app/openapi/model/models.ts +++ b/frontend/src/app/openapi/model/models.ts @@ -80,6 +80,7 @@ export * from './page-pipeline-run'; export * from './patch-project'; export * from './patch-project-user'; export * from './patch-t4-c-instance'; +export * from './patch-t4-c-model'; export * from './patch-tool-model'; export * from './patch-user'; export * from './path-validation'; diff --git a/frontend/src/app/openapi/model/patch-t4-c-instance.ts b/frontend/src/app/openapi/model/patch-t4-c-instance.ts index 1c612c107..b393f18da 100644 --- a/frontend/src/app/openapi/model/patch-t4-c-instance.ts +++ b/frontend/src/app/openapi/model/patch-t4-c-instance.ts @@ -24,7 +24,6 @@ export interface PatchT4CInstance { username?: string | null; password?: string | null; protocol?: Protocol | null; - version_id?: number | null; is_archived?: boolean | null; } export namespace PatchT4CInstance { diff --git a/frontend/src/app/openapi/model/patch-t4-c-model.ts b/frontend/src/app/openapi/model/patch-t4-c-model.ts new file mode 100644 index 000000000..7f546f683 --- /dev/null +++ b/frontend/src/app/openapi/model/patch-t4-c-model.ts @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + * + * Capella Collaboration + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * Do not edit the class manually. + + To generate a new version, run `make openapi` in the root directory of this repository. + */ + + + +export interface PatchT4CModel { + name?: string | null; + t4c_instance_id?: number | null; + t4c_repository_id?: number | null; +} + diff --git a/frontend/src/app/projects/models/model-detail/model-detail.component.html b/frontend/src/app/projects/models/model-detail/model-detail.component.html index 53983c5dc..e846295e5 100644 --- a/frontend/src/app/projects/models/model-detail/model-detail.component.html +++ b/frontend/src/app/projects/models/model-detail/model-detail.component.html @@ -5,7 +5,7 @@

Git repositories

- +
Use existing repository
@@ -23,10 +23,7 @@

Git repositories

> } @else { @for (gitModel of gitModelService.gitModels$ | async; track gitModel.id) { -
+
Integration {{ gitModel.id }} @@ -62,10 +59,7 @@

Git repositories

) {

TeamForCapella repositories

- +
Use existing repository
@@ -75,7 +69,7 @@

TeamForCapella repositories

- +
Create new repository
@@ -94,7 +88,7 @@

TeamForCapella repositories

} @else {
diff --git a/frontend/src/app/projects/models/model-source/git/manage-git-model/manage-git-model.component.ts b/frontend/src/app/projects/models/model-source/git/manage-git-model/manage-git-model.component.ts index 9a8e9670a..21528c554 100644 --- a/frontend/src/app/projects/models/model-source/git/manage-git-model/manage-git-model.component.ts +++ b/frontend/src/app/projects/models/model-source/git/manage-git-model/manage-git-model.component.ts @@ -307,7 +307,7 @@ export class ManageGitModelComponent implements OnInit, OnDestroy { if (this.asStepper) { this.create.emit(true); } else { - this.router.navigate(['../../modelsources'], { + this.router.navigate(['../..'], { relativeTo: this.route, }); } @@ -328,7 +328,7 @@ export class ManageGitModelComponent implements OnInit, OnDestroy { patchGitModel, ) .subscribe(() => - this.router.navigate(['../../modelsources'], { + this.router.navigate(['../..'], { relativeTo: this.route, }), ); diff --git a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.css b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.css deleted file mode 100644 index 8535c6938..000000000 --- a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.css +++ /dev/null @@ -1,4 +0,0 @@ -/* - * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors - * SPDX-License-Identifier: Apache-2.0 - */ diff --git a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.html b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.html index bef5f74ef..87299cbd8 100644 --- a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.html +++ b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.html @@ -3,65 +3,79 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -
-
-
- +
+ - + @if (t4cModelService.compatibleT4CInstances$ | async) { +
+ @if ((t4cModelService.compatibleT4CInstances$ | async)!.length) { + + Instance + + @for ( + instance of t4cModelService.compatibleT4CInstances$ | async; + track instance.id + ) { + + {{ instance.name }} + + } + + Only compatible servers are listed. + + } @else { +
+ No compatible T4C instance found! Make sure that the version of + the model matches the version of a TeamForCapella server + instance. +
+ } +
+ } @else { + + } -
- - Instance - - - {{ instance.name }} - - - -
+
+ + Repository + + @if (t4cRepositoryNameControl.errors?.pattern) { + + The following characters are allowed: A-Z, a-z, 0-9, _, - + + } @else if (t4cRepositoryNameControl.errors?.uniqueName) { + Repository already exists + } + +
-
- - Repository - - - Repository already exists - - - The following characters are allowed: A-Z, a-z, 0-9, _, - - - -
+
+ + T4C Project Name + + +
-
- - Project - - -
- -
- -
- +
+ +
+ +
-
+} diff --git a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.ts b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.ts index abd06867b..be35869ae 100644 --- a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.ts +++ b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.component.ts @@ -2,7 +2,7 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { NgIf, NgFor, AsyncPipe } from '@angular/common'; +import { AsyncPipe } from '@angular/common'; import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormControl, @@ -15,7 +15,7 @@ import { MatButton } from '@angular/material/button'; import { MatOption } from '@angular/material/core'; import { MatFormField, MatLabel, MatError } from '@angular/material/form-field'; import { MatIcon } from '@angular/material/icon'; -import { MatInput } from '@angular/material/input'; +import { MatInput, MatInputModule } from '@angular/material/input'; import { MatSelect } from '@angular/material/select'; import { ActivatedRoute, Router } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @@ -32,23 +32,21 @@ import { T4CModelService } from '../service/t4c-model.service'; @Component({ selector: 'app-create-t4c-model-new-repository', templateUrl: './create-t4c-model-new-repository.component.html', - styleUrls: ['./create-t4c-model-new-repository.component.css'], standalone: true, imports: [ - NgIf, FormsModule, ReactiveFormsModule, FormFieldSkeletonLoaderComponent, MatFormField, MatLabel, MatSelect, - NgFor, MatOption, MatInput, MatError, MatButton, MatIcon, AsyncPipe, + MatInputModule, ], }) export class CreateT4cModelNewRepositoryComponent implements OnInit { @@ -153,7 +151,7 @@ export class CreateT4cModelNewRepositoryComponent implements OnInit { if (this.asStepper) { this.create.emit(true); } else { - this.router.navigate(['../../modelsources'], { + this.router.navigate(['../..'], { relativeTo: this.route, }); } diff --git a/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.stories.ts b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.stories.ts new file mode 100644 index 000000000..476dbed1f --- /dev/null +++ b/frontend/src/app/projects/models/model-source/t4c/create-t4c-model-new-repository/create-t4c-model-new-repository.stories.ts @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { userEvent, within } from '@storybook/test'; +import { T4CModelService } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; +import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; +import { ProjectWrapperService } from 'src/app/projects/service/project.service'; +import { T4CRepositoryWrapperService } from 'src/app/settings/modelsources/t4c-settings/service/t4c-repos/t4c-repo.service'; +import { mockModel, MockModelWrapperService } from 'src/storybook/model'; +import { mockProject, MockProjectWrapperService } from 'src/storybook/project'; +import { + mockExtendedT4CRepository, + mockT4CInstance, + MockT4CModelService, + MockT4CRepositoryWrapperService, +} from 'src/storybook/t4c'; +import { CreateT4cModelNewRepositoryComponent } from './create-t4c-model-new-repository.component'; + +const meta: Meta = { + title: 'Model Components/Model Sources/Create T4C Model', + component: CreateT4cModelNewRepositoryComponent, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: ProjectWrapperService, + useFactory: () => + new MockProjectWrapperService(mockProject, [mockProject]), + }, + { + provide: ModelWrapperService, + useFactory: () => new MockModelWrapperService(mockModel, [mockModel]), + }, + ], + }), + ], +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const General: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CModelService, + useFactory: () => + new MockT4CModelService(undefined, undefined, [mockT4CInstance]), + }, + { + provide: T4CRepositoryWrapperService, + useFactory: () => + new MockT4CRepositoryWrapperService([mockExtendedT4CRepository]), + }, + ], + }), + ], +}; + +export const NoInstances: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CModelService, + useFactory: () => new MockT4CModelService(undefined, undefined, []), + }, + ], + }), + ], +}; + +export const InstanceSelected: Story = { + args: {}, + play: async ({ canvasElement }) => { + await userEvent.click(within(canvasElement).getByTestId('t4c-instance')); + + const generalDropdownItem = document.querySelector( + 'mat-option[ng-reflect-value="1"]', + ); + if (!generalDropdownItem) { + throw new Error('Dropdown item with value "1" not found'); + } + await userEvent.click(generalDropdownItem); + }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CModelService, + useFactory: () => + new MockT4CModelService(undefined, undefined, [mockT4CInstance]), + }, + { + provide: T4CRepositoryWrapperService, + useFactory: () => + new MockT4CRepositoryWrapperService([mockExtendedT4CRepository]), + }, + ], + }), + ], +}; diff --git a/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.html b/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.html index af7304369..77b8b37c6 100644 --- a/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.html +++ b/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.html @@ -6,55 +6,79 @@ @if ((projectService.project$ | async) && (modelService.model$ | async)) {
-
+
- @if ((t4cInstanceService.t4cInstances$ | async) === undefined) { - - } @else { - - Instance - - @for ( - instance of t4cInstanceService.t4cInstances$ | async; - track instance.id - ) { - - {{ instance.name }} - + @if (t4cModelService.compatibleT4CInstances$ | async) { + @if ((t4cModelService.compatibleT4CInstances$ | async)!.length) { + + Instance + + @for ( + instance of t4cModelService.compatibleT4CInstances$ | async; + track instance.id + ) { + + {{ instance.name }} + + } + + @if (form.controls.t4cInstanceId.errors) { + Please select an instance. + } @else { + Only compatible servers are listed. } - - @if (form.controls.t4cInstanceId.errors) { - Please select an instance. - } - + + } @else { +
+ No compatible T4C instance found! Make sure that the version of + the model matches the version of a TeamForCapella server + instance. +
+ } + } @else { + }
- @if ((t4cInstanceService.t4cInstances$ | async) === undefined) { - - } @else { - - Repository - - @for ( - repository of t4cRepositoryService.repositories$ | async; - track repository.id - ) { - - {{ repository.name }} - + @if (t4cModelService.compatibleT4CInstances$ | async) { + @if ((t4cRepositoryService.repositories$ | async)?.length === 0) { +
+ No T4C repository found for the selected instance! Make sure to + create a repository first. +
+ } @else { + + Repository + + @for ( + repository of t4cRepositoryService.repositories$ | async; + track repository.id + ) { + + {{ repository.name }} + + } + + @if (form.controls.t4cRepositoryId.errors) { + Please select a repository. } -
- @if (form.controls.t4cRepositoryId.errors) { - Please select a repository. - } -
+ + } + } @else { + }
@@ -62,7 +86,7 @@ } @else { - T4C project name + T4C Project Name @if (form.controls.name.errors?.alreadyUsed) { @@ -81,7 +105,7 @@
-
+
@if (t4cModel !== undefined) { }
diff --git a/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.ts b/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.ts index a1d441144..aa873ee84 100644 --- a/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.ts +++ b/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.component.ts @@ -18,17 +18,13 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; -import { - MatAutocompleteTrigger, - MatAutocomplete, -} from '@angular/material/autocomplete'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButton } from '@angular/material/button'; -import { MatOption } from '@angular/material/core'; import { MatDialog } from '@angular/material/dialog'; -import { MatFormField, MatLabel, MatError } from '@angular/material/form-field'; +import { MatFormField } from '@angular/material/form-field'; import { MatIcon } from '@angular/material/icon'; -import { MatInput } from '@angular/material/input'; -import { MatSelect } from '@angular/material/select'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; import { MatTooltip } from '@angular/material/tooltip'; import { ActivatedRoute, Router } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; @@ -36,11 +32,8 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { combineLatest, filter } from 'rxjs'; import { ConfirmationDialogComponent } from 'src/app/helpers/confirmation-dialog/confirmation-dialog.component'; import { ToastService } from 'src/app/helpers/toast/toast.service'; -import { - SubmitT4CModel, - T4CModel, - T4CModelService, -} from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; +import { SubmitT4CModel, T4CModel } from 'src/app/openapi'; +import { T4CModelService } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; import { ProjectWrapperService } from 'src/app/projects/service/project.service'; import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; @@ -60,14 +53,10 @@ import { FormFieldSkeletonLoaderComponent } from '../../../../../helpers/skeleto ReactiveFormsModule, FormFieldSkeletonLoaderComponent, MatFormField, - MatLabel, - MatSelect, - MatOption, + MatSelectModule, MatTooltip, - MatError, - MatInput, - MatAutocompleteTrigger, - MatAutocomplete, + MatInputModule, + MatAutocompleteModule, MatButton, MatIcon, AsyncPipe, @@ -216,7 +205,7 @@ export class ManageT4CModelComponent implements OnInit, OnDestroy { if (this.asStepper) { this.create.emit(true); } else { - this.router.navigate(['../../modelsources'], { + this.router.navigate(['../..'], { relativeTo: this.route, }); } diff --git a/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.stories.ts b/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.stories.ts index 19bba19c7..de4f7070a 100644 --- a/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.stories.ts +++ b/frontend/src/app/projects/models/model-source/t4c/manage-t4c-model/manage-t4c-model.stories.ts @@ -3,22 +3,24 @@ * SPDX-License-Identifier: Apache-2.0 */ import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { userEvent, within } from '@storybook/test'; +import { T4CModelService } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; import { ProjectWrapperService } from 'src/app/projects/service/project.service'; -import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; import { T4CRepositoryWrapperService } from 'src/app/settings/modelsources/t4c-settings/service/t4c-repos/t4c-repo.service'; import { mockModel, MockModelWrapperService } from 'src/storybook/model'; import { mockProject, MockProjectWrapperService } from 'src/storybook/project'; import { mockExtendedT4CRepository, mockT4CInstance, - MockT4CInstanceWrapperService, + mockT4CModel, + MockT4CModelService, MockT4CRepositoryWrapperService, } from 'src/storybook/t4c'; import { ManageT4CModelComponent } from './manage-t4c-model.component'; const meta: Meta = { - title: 'Model Components/Model Sources/T4C', + title: 'Model Components/Model Sources/Update T4C Model', component: ManageT4CModelComponent, decorators: [ moduleMetadata({ @@ -50,11 +52,75 @@ export const General: Story = { moduleMetadata({ providers: [ { - provide: T4CInstanceWrapperService, + provide: T4CModelService, useFactory: () => - new MockT4CInstanceWrapperService(mockT4CInstance, [ - mockT4CInstance, - ]), + new MockT4CModelService(undefined, undefined, [mockT4CInstance]), + }, + { + provide: T4CRepositoryWrapperService, + useFactory: () => + new MockT4CRepositoryWrapperService([mockExtendedT4CRepository]), + }, + ], + }), + ], +}; + +export const NoRepository: Story = { + args: {}, + play: async ({ canvasElement }) => { + await userEvent.click(within(canvasElement).getByTestId('t4c-instance')); + + const generalDropdownItem = document.querySelector( + 'mat-option[ng-reflect-value="1"]', + ); + if (!generalDropdownItem) { + throw new Error('Dropdown item with value "1" not found'); + } + await userEvent.click(generalDropdownItem); + }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CModelService, + useFactory: () => + new MockT4CModelService(undefined, undefined, [mockT4CInstance]), + }, + { + provide: T4CRepositoryWrapperService, + useFactory: () => new MockT4CRepositoryWrapperService([]), + }, + ], + }), + ], +}; + +export const NoInstances: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CModelService, + useFactory: () => new MockT4CModelService(undefined, undefined, []), + }, + ], + }), + ], +}; + +export const Modify: Story = { + args: { + t4cModel: mockT4CModel, + }, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CModelService, + useFactory: () => + new MockT4CModelService(mockT4CModel, undefined, [mockT4CInstance]), }, { provide: T4CRepositoryWrapperService, diff --git a/frontend/src/app/projects/models/model-source/t4c/service/t4c-model.service.ts b/frontend/src/app/projects/models/model-source/t4c/service/t4c-model.service.ts index 25eb1f5be..2a7c6ca5c 100644 --- a/frontend/src/app/projects/models/model-source/t4c/service/t4c-model.service.ts +++ b/frontend/src/app/projects/models/model-source/t4c/service/t4c-model.service.ts @@ -2,22 +2,37 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { AbstractControl, AsyncValidatorFn, ValidationErrors, } from '@angular/forms'; -import { BehaviorSubject, Observable, map, take, tap } from 'rxjs'; -import { T4CRepository } from 'src/app/openapi'; -import { environment } from 'src/environments/environment'; +import { + BehaviorSubject, + Observable, + combineLatest, + map, + take, + tap, +} from 'rxjs'; +import { + ProjectsModelsT4CService, + SubmitT4CModel, + T4CModel, +} from 'src/app/openapi'; +import { ModelWrapperService } from 'src/app/projects/models/service/model.service'; +import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; @Injectable({ providedIn: 'root', }) export class T4CModelService { - constructor(private http: HttpClient) {} + constructor( + private modelWrapperService: ModelWrapperService, + private t4cInstanceWrapperService: T4CInstanceWrapperService, + private t4cModelService: ProjectsModelsT4CService, + ) {} private _t4cModel = new BehaviorSubject(undefined); public readonly t4cModel$ = this._t4cModel.asObservable(); @@ -25,26 +40,32 @@ export class T4CModelService { private _t4cModels = new BehaviorSubject(undefined); public readonly t4cModels$ = this._t4cModels.asObservable(); - urlFactory(projectSlug: string, modelSlug: string): string { - return `${environment.backend_url}/projects/${projectSlug}/models/${modelSlug}/modelsources/t4c`; - } + compatibleT4CInstances$ = combineLatest([ + this.modelWrapperService.model$, + this.t4cInstanceWrapperService.t4cInstances$, + ]).pipe( + map(([model, instances]) => { + return instances?.filter( + (instance) => + model?.version?.config.compatible_versions.includes( + instance.version.id, + ) || model?.version?.id === instance.version.id, + ); + }), + ); loadT4CModels(projectSlug: string, modelSlug: string): void { - this.http - .get(this.urlFactory(projectSlug, modelSlug)) - .subscribe({ - next: (models) => this._t4cModels.next(models), - error: () => this._t4cModels.next(undefined), - }); + this.t4cModelService.listT4cModels(projectSlug, modelSlug).subscribe({ + next: (models) => this._t4cModels.next(models), + error: () => this._t4cModels.next(undefined), + }); } loadT4CModel(projectSlug: string, modelSlug: string, id: number): void { - this.http - .get(`${this.urlFactory(projectSlug, modelSlug)}/${id}`) - .subscribe({ - next: (model) => this._t4cModel.next(model), - error: () => this._t4cModel.next(undefined), - }); + this.t4cModelService.getT4cModel(projectSlug, id, modelSlug).subscribe({ + next: (model) => this._t4cModel.next(model), + error: () => this._t4cModel.next(undefined), + }); } createT4CModel( @@ -52,8 +73,8 @@ export class T4CModelService { modelSlug: string, body: SubmitT4CModel, ): Observable { - return this.http - .post(this.urlFactory(projectSlug, modelSlug), body) + return this.t4cModelService + .createT4cModel(projectSlug, modelSlug, body) .pipe( tap((model) => { this._t4cModel.next(model); @@ -65,14 +86,11 @@ export class T4CModelService { patchT4CModel( projectSlug: string, modelSlug: string, - t4c_model_id: number, + t4cModelID: number, body: SubmitT4CModel, ): Observable { - return this.http - .patch( - `${this.urlFactory(projectSlug, modelSlug)}/${t4c_model_id}`, - body, - ) + return this.t4cModelService + .updateT4cModel(projectSlug, t4cModelID, modelSlug, body) .pipe( tap((model) => { this._t4cModel.next(model); @@ -86,8 +104,8 @@ export class T4CModelService { modelSlug: string, t4cModelId: number, ): Observable { - return this.http - .delete(`${this.urlFactory(projectSlug, modelSlug)}/${t4cModelId}`) + return this.t4cModelService + .deleteT4cModel(projectSlug, t4cModelId, modelSlug) .pipe( tap(() => { this._t4cModel.next(undefined); @@ -121,21 +139,3 @@ export class T4CModelService { }; } } - -export interface SubmitT4CModel { - t4c_instance_id: number; - t4c_repository_id: number; - name: string; -} - -export interface T4CModel { - name: string; - id: number; - repository: T4CRepository; -} - -export interface SimpleT4CModel { - project_name: string; - repository_name: string; - instance_name: string; -} diff --git a/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.html b/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.html index 73277376a..a3ee49128 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.html +++ b/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.html @@ -43,11 +43,10 @@

@if ( (t4cInstanceWrapperService.t4cInstance$ | async) !== undefined && - (t4cInstanceWrapperService.t4cInstance$ | async)!.version.id !== - form.value.version_id + editing ) { Models are not auto-migrated on version change.The version can't be updated. } @else if ( form.controls.version_id.errors?.required || @@ -180,8 +179,6 @@

The password is required. } @else if (existing && !form.value.password) { Is not changed if empty - } @else { - } diff --git a/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.ts b/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.ts index db12916de..2a75a41b8 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.ts +++ b/frontend/src/app/settings/modelsources/t4c-settings/edit-t4c-instance/edit-t4c-instance.component.ts @@ -158,6 +158,7 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { enableEditing(): void { this.editing = true; this.form.enable(); + this.form.controls.version_id.disable(); this.form.controls.password.patchValue(''); this.form.controls.password.removeValidators(Validators.required); diff --git a/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.html b/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.html index b0abeff08..953df5965 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.html +++ b/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.html @@ -115,7 +115,7 @@

Repositories

class="!h-[56px]" mat-stroked-button color="primary" - [disabled]="!form.valid" + [disabled]="!form.valid || repositoryCreationInProgress" (click)="createRepository()" >
diff --git a/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.ts b/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.ts index c338b4ffd..bb3357a5e 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.ts +++ b/frontend/src/app/settings/modelsources/t4c-settings/t4c-instance-settings/t4c-instance-settings.component.ts @@ -65,6 +65,8 @@ export class T4CInstanceSettingsComponent implements OnChanges, OnDestroy { search = ''; + repositoryCreationInProgress = false; + constructor( public t4cRepoService: T4CRepositoryWrapperService, private dialog: MatDialog, @@ -102,13 +104,20 @@ export class T4CInstanceSettingsComponent implements OnChanges, OnDestroy { } createRepository(): void { + this.repositoryCreationInProgress = true; if (this.form.valid) { this.t4cRepoService .createRepository( this.instance!.id, this.form.value as CreateT4CRepository, ) - .subscribe(() => this.form.reset()); + .subscribe({ + next: () => { + this.form.reset(); + this.repositoryCreationInProgress = false; + }, + error: () => (this.repositoryCreationInProgress = false), + }); } } diff --git a/frontend/src/storybook/t4c.ts b/frontend/src/storybook/t4c.ts index 3d3950a98..11fd4c68e 100644 --- a/frontend/src/storybook/t4c.ts +++ b/frontend/src/storybook/t4c.ts @@ -9,16 +9,14 @@ import { } from '@angular/forms'; import { BehaviorSubject, Observable, of } from 'rxjs'; import { + SimpleT4CModel, T4CInstance, T4CModel, T4CRepository, T4CRepositoryStatus, ValidationError, } from 'src/app/openapi'; -import { - SimpleT4CModel, - T4CModelService, -} from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; +import { T4CModelService } from 'src/app/projects/models/model-source/t4c/service/t4c-model.service'; import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; import { ExtendedT4CRepository, @@ -110,6 +108,7 @@ export class MockT4CRepositoryWrapperService } reset() {} // eslint-disable-line @typescript-eslint/no-empty-function + loadRepositories() {} // eslint-disable-line @typescript-eslint/no-empty-function asyncNameValidator(): AsyncValidatorFn { return (_control: AbstractControl): Observable => { @@ -125,10 +124,18 @@ export class MockT4CModelService implements Partial { private _t4cModels = new BehaviorSubject(undefined); public readonly t4cModels$ = this._t4cModels.asObservable(); + compatibleT4CInstances$: Observable = + of(undefined); + reset() {} // eslint-disable-line @typescript-eslint/no-empty-function - constructor(t4cModel: T4CModel, t4cModels: T4CModel[]) { + constructor( + t4cModel: T4CModel | undefined, + t4cModels: T4CModel[] | undefined, + t4cInstances: T4CInstance[] | undefined = undefined, + ) { this._t4cModel.next(t4cModel); this._t4cModels.next(t4cModels); + this.compatibleT4CInstances$ = of(t4cInstances); } }