From 60235ce7ef565c0b7c563798de7bab37da6fe845 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Tue, 10 Sep 2024 16:33:26 +0200 Subject: [PATCH] feat: Delete TeamForCapella server instances --- .../settings/modelsources/t4c/crud.py | 8 + .../settings/modelsources/t4c/routes.py | 13 + .../teamforcapella/test_t4c_instances.py | 22 ++ .../api/settings-modelsources-t4-c.service.ts | 71 +++++ .../services/settings/t4c-instance.service.ts | 1 + .../edit-t4c-instance.component.html | 246 +++++++++--------- .../edit-t4c-instance.component.ts | 76 ++++-- .../t4c-settings/t4c-settings.component.html | 64 +++-- .../t4c-settings/t4c-settings.component.ts | 6 +- .../t4c-settings/t4c-settings.stories.ts | 41 +++ 10 files changed, 374 insertions(+), 174 deletions(-) create mode 100644 frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.stories.ts diff --git a/backend/capellacollab/settings/modelsources/t4c/crud.py b/backend/capellacollab/settings/modelsources/t4c/crud.py index 9b4b6c30b..c1369e066 100644 --- a/backend/capellacollab/settings/modelsources/t4c/crud.py +++ b/backend/capellacollab/settings/modelsources/t4c/crud.py @@ -71,3 +71,11 @@ def update_t4c_instance( db.commit() return instance + + +def delete_t4c_instance( + db: orm.Session, + instance: models.DatabaseT4CInstance, +): + db.delete(instance) + db.commit() diff --git a/backend/capellacollab/settings/modelsources/t4c/routes.py b/backend/capellacollab/settings/modelsources/t4c/routes.py index fa71b4e84..dd0c1ec17 100644 --- a/backend/capellacollab/settings/modelsources/t4c/routes.py +++ b/backend/capellacollab/settings/modelsources/t4c/routes.py @@ -91,6 +91,19 @@ def edit_t4c_instance( return crud.update_t4c_instance(db, instance, body) +@router.delete( + "/{t4c_instance_id}", + status_code=204, +) +def delete_t4c_instance( + instance: models.DatabaseT4CInstance = fastapi.Depends( + injectables.get_existing_instance + ), + db: orm.Session = fastapi.Depends(database.get_db), +): + crud.delete_t4c_instance(db, instance) + + @router.get( "/{t4c_instance_id}/licenses", response_model=models.GetSessionUsageResponse, diff --git a/backend/tests/settings/teamforcapella/test_t4c_instances.py b/backend/tests/settings/teamforcapella/test_t4c_instances.py index 89fd9cde3..b26d8fe73 100644 --- a/backend/tests/settings/teamforcapella/test_t4c_instances.py +++ b/backend/tests/settings/teamforcapella/test_t4c_instances.py @@ -6,6 +6,12 @@ from fastapi import status, testclient from sqlalchemy import orm +from capellacollab.projects.toolmodels.modelsources.t4c import ( + crud as models_t4c_crud, +) +from capellacollab.projects.toolmodels.modelsources.t4c import ( + models as models_t4c_models, +) from capellacollab.settings.modelsources.t4c import crud as t4c_crud from capellacollab.settings.modelsources.t4c import ( exceptions as settings_t4c_exceptions, @@ -260,6 +266,22 @@ def test_patch_t4c_instance_already_existing_name( assert "name already used" in detail["title"] +@pytest.mark.usefixtures("admin") +def test_delete_t4c_instance( + client: testclient.TestClient, + db: orm.Session, + t4c_instance: t4c_models.DatabaseT4CInstance, + t4c_model: models_t4c_models.DatabaseT4CModel, +): + response = client.delete( + f"/api/v1/settings/modelsources/t4c/{t4c_instance.id}", + ) + + assert response.status_code == 204 + assert t4c_crud.get_t4c_instance_by_id(db, t4c_instance.id) is None + assert models_t4c_crud.get_t4c_model_by_id(db, t4c_model.id) is None + + def test_injectables_raise_when_archived_instance( db: orm.Session, executor_name: str, diff --git a/frontend/src/app/openapi/api/settings-modelsources-t4-c.service.ts b/frontend/src/app/openapi/api/settings-modelsources-t4-c.service.ts index ad06b5d7f..da91d0402 100644 --- a/frontend/src/app/openapi/api/settings-modelsources-t4-c.service.ts +++ b/frontend/src/app/openapi/api/settings-modelsources-t4-c.service.ts @@ -274,6 +274,77 @@ export class SettingsModelsourcesT4CService { ); } + /** + * Delete T4C Instance + * @param t4cInstanceId + * @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 deleteT4cInstance(t4cInstanceId: number, observe?: 'body', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable; + public deleteT4cInstance(t4cInstanceId: number, observe?: 'response', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteT4cInstance(t4cInstanceId: number, observe?: 'events', reportProgress?: boolean, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable>; + public deleteT4cInstance(t4cInstanceId: number, observe: any = 'body', reportProgress: boolean = false, options?: {httpHeaderAccept?: 'application/json', context?: HttpContext, transferCache?: boolean}): Observable { + if (t4cInstanceId === null || t4cInstanceId === undefined) { + throw new Error('Required parameter t4cInstanceId was null or undefined when calling deleteT4cInstance.'); + } + + let localVarHeaders = this.defaultHeaders; + + let localVarCredential: string | undefined; + // authentication (PersonalAccessToken) required + localVarCredential = this.configuration.lookupCredential('PersonalAccessToken'); + if (localVarCredential) { + localVarHeaders = localVarHeaders.set('Authorization', 'Basic ' + localVarCredential); + } + + let localVarHttpHeaderAcceptSelected: string | undefined = options && options.httpHeaderAccept; + if (localVarHttpHeaderAcceptSelected === undefined) { + // to determine the Accept header + const httpHeaderAccepts: string[] = [ + 'application/json' + ]; + localVarHttpHeaderAcceptSelected = this.configuration.selectHeaderAccept(httpHeaderAccepts); + } + if (localVarHttpHeaderAcceptSelected !== undefined) { + localVarHeaders = localVarHeaders.set('Accept', localVarHttpHeaderAcceptSelected); + } + + let localVarHttpContext: HttpContext | undefined = options && options.context; + if (localVarHttpContext === undefined) { + localVarHttpContext = new HttpContext(); + } + + let localVarTransferCache: boolean | undefined = options && options.transferCache; + if (localVarTransferCache === undefined) { + localVarTransferCache = true; + } + + + let responseType_: 'text' | 'json' | 'blob' = 'json'; + if (localVarHttpHeaderAcceptSelected) { + if (localVarHttpHeaderAcceptSelected.startsWith('text')) { + responseType_ = 'text'; + } else if (this.configuration.isJsonMime(localVarHttpHeaderAcceptSelected)) { + responseType_ = 'json'; + } else { + responseType_ = 'blob'; + } + } + + let localVarPath = `/api/v1/settings/modelsources/t4c/${this.configuration.encodeParam({name: "t4cInstanceId", value: t4cInstanceId, in: "path", style: "simple", explode: false, dataType: "number", dataFormat: undefined})}`; + return this.httpClient.request('delete', `${this.configuration.basePath}${localVarPath}`, + { + context: localVarHttpContext, + responseType: responseType_, + withCredentials: this.configuration.withCredentials, + headers: localVarHeaders, + observe: observe, + transferCache: localVarTransferCache, + reportProgress: reportProgress + } + ); + } + /** * Delete T4C Repository * @param t4cInstanceId diff --git a/frontend/src/app/services/settings/t4c-instance.service.ts b/frontend/src/app/services/settings/t4c-instance.service.ts index a469d6bd4..c24e6e98b 100644 --- a/frontend/src/app/services/settings/t4c-instance.service.ts +++ b/frontend/src/app/services/settings/t4c-instance.service.ts @@ -40,6 +40,7 @@ export class T4CInstanceWrapperService { ); loadInstances(): void { + this._t4cInstances.next(undefined); this.t4cInstanceService.getT4cInstances().subscribe({ next: (instances) => this._t4cInstances.next(instances), error: () => this._t4cInstances.next(undefined), 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 5f447366d..73277376a 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 @@ -24,33 +24,36 @@

Name - - The name is required. - - An instance with the same name already exists. - + @if (form.controls.name.errors?.required) { + The name is required. + } @else if (form.controls.name.errors?.uniqueName) { + + An instance with the same name already exists. + + } Capella version - - {{ version.name }} - + @for (version of capellaVersions; track version.id) { + + {{ version.name }} + + } @if ( - (t4cInstanceService.t4cInstance$ | async) !== undefined && - (t4cInstanceService.t4cInstance$ | async)!.version.id !== + (t4cInstanceWrapperService.t4cInstance$ | async) !== undefined && + (t4cInstanceWrapperService.t4cInstance$ | async)!.version.id !== form.value.version_id ) { Models are not auto-migrated on version change. - } @else if (form.controls.version_id.errors?.required) { - The version is required. + } @else if ( + form.controls.version_id.errors?.required || + form.controls.version_id.errors?.min + ) { + The version is required. } @@ -59,7 +62,7 @@

License configuration @if (form.controls.license.errors?.required) { - The license configuration is required. + The license configuration is required. } @@ -71,11 +74,9 @@

> Protocol - {{ protocol }} + @for (protocol of protocols; track protocol) { + {{ protocol }} + } @@ -92,19 +93,15 @@

> Port - - Valid ports are between 0 and 65535. - - - We only support numerical ports :( - - - The port is required. - + @if ( + form.controls.port.errors?.min || form.controls.port.errors?.max + ) { + Valid ports are between 0 and 65535. + } @else if (form.controls.port.errors?.pattern) { + Only numerical ports are supported. + } @else if (form.controls.port.errors?.required) { + The port is required. + } > CDO port - - Valid CDO ports are between 0 and 65535. - - - We only support numerical CDO ports :( - - - The CDO port is required. - + @if (form.controls.cdo_port.errors?.required) { + The CDO port is required. + } @else if (form.controls.cdo_port.errors?.pattern) { + Only numerical ports are supported. + } @else if ( + form.controls.cdo_port.errors?.min || + form.controls.cdo_port.errors?.max + ) { + Valid CDO ports are between 0 and 65535. + } > HTTP port - - Valid ports are between 0 and 65535. - - - We only support numerical ports :( - + @if (form.controls.http_port.errors?.pattern) { + Only numerical ports are supported. + } @else if ( + form.controls.http_port.errors?.min || + form.controls.http_port.errors?.max + ) { + Valid ports are between 0 and 65535. + }
License server API - - The URL should start with “http(s)://” - - - The license server API is required. - + @if (form.controls.usage_api.errors?.required) { + The license server API is required. + } @else if (form.controls.usage_api.errors?.pattern) { + The URL should start with "http(s)://" + } Experimental REST API - - The URL should start with “http(s)://” - - - The REST server URL is required. - + @if (form.controls.rest_api.errors?.required) { + The REST server URL is required. + } @else if (form.controls.rest_api.errors?.pattern) { + The URL should start with "http(s)://" + }
Username - - The username is required. - + @if (form.controls.username.errors?.required) { + The username is required. + } Password @@ -188,67 +176,73 @@

autocomplete="new-password" formControlName="password" /> - - The password is required. - - Is not changed if empty + @if (form.controls.password.errors?.required) { + The password is required. + } @else if (existing && !form.value.password) { + Is not changed if empty + } @else { + + }

-
- - -
-
-
- -
- -
-
- -
-
- + } @else if (editing) { + + + } @else if ( + (t4cInstanceWrapperService.t4cInstance$ | async) !== undefined + ) { + @if (!isArchived) { + + } @else { +
+ } + + +
+ }
- @if ((t4cInstanceService.t4cInstance$ | async) !== undefined) { + @if ((t4cInstanceWrapperService.t4cInstance$ | async) !== undefined) { } 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 3fb191fee..db12916de 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 @@ -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, OnDestroy, OnInit } from '@angular/core'; import { FormControl, @@ -13,6 +13,7 @@ import { } from '@angular/forms'; import { MatButton } from '@angular/material/button'; import { MatOption } from '@angular/material/core'; +import { MatDialog } from '@angular/material/dialog'; import { MatFormField, MatLabel, @@ -24,13 +25,15 @@ import { MatInput } from '@angular/material/input'; import { MatSelect } from '@angular/material/select'; import { ActivatedRoute, Router } from '@angular/router'; import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy'; -import { filter, map } from 'rxjs'; +import { filter, map, take } from 'rxjs'; import { BreadcrumbsService } from 'src/app/general/breadcrumbs/breadcrumbs.service'; +import { ConfirmationDialogComponent } from 'src/app/helpers/confirmation-dialog/confirmation-dialog.component'; import { ToastService } from 'src/app/helpers/toast/toast.service'; import { CreateT4CInstance, PatchT4CInstance, Protocol, + SettingsModelsourcesT4CService, ToolVersion, } from 'src/app/openapi'; import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; @@ -44,7 +47,6 @@ import { T4CInstanceSettingsComponent } from '../t4c-instance-settings/t4c-insta templateUrl: './edit-t4c-instance.component.html', standalone: true, imports: [ - NgIf, FormsModule, ReactiveFormsModule, MatFormField, @@ -52,7 +54,6 @@ import { T4CInstanceSettingsComponent } from '../t4c-instance-settings/t4c-insta MatInput, MatError, MatSelect, - NgFor, MatOption, MatHint, MatButton, @@ -80,18 +81,18 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { public form = new FormGroup({ name: new FormControl('', { validators: Validators.required, - asyncValidators: this.t4cInstanceService.asyncNameValidator(), + asyncValidators: this.t4cInstanceWrapperService.asyncNameValidator(), }), - version_id: new FormControl(-1, Validators.required), + version_id: new FormControl(-1, [Validators.required, Validators.min(0)]), license: new FormControl('', Validators.required), - protocol: new FormControl('tcp', Validators.required), + protocol: new FormControl('ws', Validators.required), host: new FormControl('', Validators.required), port: new FormControl(2036, [Validators.required, ...this.portValidators]), cdo_port: new FormControl(12036, [ Validators.required, ...this.portValidators, ]), - http_port: new FormControl(null, this.portValidators), + http_port: new FormControl(8080, this.portValidators), usage_api: new FormControl('', [ Validators.required, Validators.pattern(/^https?:\/\//), @@ -104,13 +105,19 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { password: new FormControl('', [Validators.required]), }); + get protocols(): Protocol[] { + return Object.values(Protocol); + } + constructor( - public t4cInstanceService: T4CInstanceWrapperService, + public t4cInstanceWrapperService: T4CInstanceWrapperService, + private t4cInstanceService: SettingsModelsourcesT4CService, private route: ActivatedRoute, private router: Router, private toastService: ToastService, private toolService: ToolWrapperService, private breadcrumbsService: BreadcrumbsService, + private dialog: MatDialog, ) {} ngOnInit(): void { @@ -124,10 +131,10 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { this.form.disable(); this.instanceId = instanceId; - this.t4cInstanceService.loadInstance(instanceId); + this.t4cInstanceWrapperService.loadInstance(instanceId); }); - this.t4cInstanceService.t4cInstance$ + this.t4cInstanceWrapperService.t4cInstance$ .pipe(untilDestroyed(this), filter(Boolean)) .subscribe((initialT4CInstance) => { const t4cInstance = { @@ -137,7 +144,7 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { this.isArchived = t4cInstance.is_archived; this.form.patchValue(t4cInstance); this.form.controls.name.setAsyncValidators( - this.t4cInstanceService.asyncNameValidator(t4cInstance), + this.t4cInstanceWrapperService.asyncNameValidator(t4cInstance), ); this.breadcrumbsService.updatePlaceholder({ t4cInstance }); }); @@ -162,13 +169,13 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { this.form.disable(); if (this.instanceId) { - this.t4cInstanceService.loadInstance(this.instanceId); + this.t4cInstanceWrapperService.loadInstance(this.instanceId); } } create(): void { if (this.form.valid) { - this.t4cInstanceService + this.t4cInstanceWrapperService .createInstance(this.form.value as CreateT4CInstance) .subscribe((instance) => { this.toastService.showSuccess( @@ -184,7 +191,7 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { update(): void { if (this.form.valid && this.instanceId) { - this.t4cInstanceService + this.t4cInstanceWrapperService .updateInstance(this.instanceId, this.form.value as PatchT4CInstance) .subscribe((instance) => { this.editing = false; @@ -199,7 +206,7 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { toggleArchive(): void { if (this.instanceId) { - this.t4cInstanceService + this.t4cInstanceWrapperService .updateInstance(this.instanceId, { is_archived: !this.isArchived, }) @@ -224,7 +231,42 @@ export class EditT4CInstanceComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.t4cInstanceService.resetT4CInstance(); + this.t4cInstanceWrapperService.resetT4CInstance(); this.breadcrumbsService.updatePlaceholder({ t4cInstance: undefined }); } + + deleteT4CRepository(): void { + this.t4cInstanceWrapperService.t4cInstance$ + .pipe(take(1), untilDestroyed(this)) + .subscribe((instance) => { + if (!instance) { + return; + } + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { + data: { + title: 'Delete TeamForCapella Server Instance', + text: + `Do you really want to remove the integration of the TeamForCapella server '${instance?.name}'? ` + + 'This will remove all integrations of related repositories in projects. ' + + 'Repositories will no longer be injected into sessions and no session token will be issues for the repository anymore. ' + + 'The server itself will not be removed, only the link between the Capella Collaboration Manager and the TeamForCapella server.', + }, + }); + + dialogRef.afterClosed().subscribe((result: boolean) => { + if (result) { + this.t4cInstanceService.deleteT4cInstance(instance.id).subscribe({ + next: () => { + this.toastService.showSuccess( + `TeamForCapella instance removed`, + `The TeamForCapella instance '${instance.name}' and all related repositories have been removed.`, + ); + this.t4cInstanceWrapperService.loadInstances(); + this.router.navigateByUrl('/settings/modelsources/t4c'); + }, + }); + } + }); + }); + } } diff --git a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html index eacaba204..2399cfab1 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html +++ b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.html @@ -3,8 +3,8 @@ ~ SPDX-License-Identifier: Apache-2.0 --> -
- +
+
diff --git a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.ts b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.ts index 6b9870ff1..c0889dafa 100644 --- a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.ts +++ b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.component.ts @@ -2,11 +2,12 @@ * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors * SPDX-License-Identifier: Apache-2.0 */ -import { NgFor, NgClass, NgIf, AsyncPipe } from '@angular/common'; +import { NgClass, AsyncPipe } from '@angular/common'; import { Component } from '@angular/core'; import { MatRipple } from '@angular/material/core'; import { MatIcon } from '@angular/material/icon'; import { RouterLink } from '@angular/router'; +import { MatCardOverviewSkeletonLoaderComponent } from 'src/app/helpers/skeleton-loaders/mat-card-overview-skeleton-loader/mat-card-overview-skeleton-loader.component'; import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; import { MatIconComponent } from '../../../helpers/mat-icon/mat-icon.component'; @@ -19,11 +20,10 @@ import { MatIconComponent } from '../../../helpers/mat-icon/mat-icon.component'; RouterLink, MatRipple, MatIconComponent, - NgFor, NgClass, MatIcon, - NgIf, AsyncPipe, + MatCardOverviewSkeletonLoaderComponent, ], }) export class T4CSettingsComponent { diff --git a/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.stories.ts b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.stories.ts new file mode 100644 index 000000000..882d3d746 --- /dev/null +++ b/frontend/src/app/settings/modelsources/t4c-settings/t4c-settings.stories.ts @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; +import { T4CInstanceWrapperService } from 'src/app/services/settings/t4c-instance.service'; +import { + mockT4CInstance, + MockT4CInstanceWrapperService, +} from 'src/storybook/t4c'; +import { T4CSettingsComponent } from './t4c-settings.component'; + +const meta: Meta = { + title: 'Settings Components / Modelsources / T4C / Server Overview', + component: T4CSettingsComponent, +}; + +export default meta; +type Story = StoryObj; + +export const Loading: Story = { + args: {}, +}; + +export const Overview: Story = { + args: {}, + decorators: [ + moduleMetadata({ + providers: [ + { + provide: T4CInstanceWrapperService, + useFactory: () => + new MockT4CInstanceWrapperService(mockT4CInstance, [ + mockT4CInstance, + { ...mockT4CInstance, is_archived: true }, + ]), + }, + ], + }), + ], +};