diff --git a/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts b/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts index 39e08b4f3..f322c650c 100644 --- a/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts +++ b/AMW_angular/io/src/app/resources/contexts-list/contexts-list.component.ts @@ -9,6 +9,7 @@ import { map } from 'rxjs/operators'; @Component({ selector: 'app-contexts-list', + standalone: true, imports: [NgClass, UpperCasePipe], templateUrl: './contexts-list.component.html', styleUrl: './contexts-list.component.scss', diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html index 6504d9b59..98bab997e 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.html @@ -37,6 +37,12 @@
+ diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts index f943e6a04..5b41692de 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-edit.component.ts @@ -14,6 +14,7 @@ import { ButtonComponent } from '../../shared/button/button.component'; import { NgbDropdown, NgbDropdownItem, NgbDropdownMenu, NgbDropdownToggle } from '@ng-bootstrap/ng-bootstrap'; import { ContextsListComponent } from '../contexts-list/contexts-list.component'; import { ResourcePropertiesComponent } from './resource-properties/resource-properties.component'; +import { ResourceReleasesComponent } from './resource-releases/resource-releases.component'; @Component({ selector: 'app-resource-edit', @@ -30,6 +31,7 @@ import { ResourcePropertiesComponent } from './resource-properties/resource-prop NgbDropdownItem, ContextsListComponent, ResourcePropertiesComponent, + ResourceReleasesComponent, ], templateUrl: './resource-edit.component.html', styleUrl: './resource-edit.component.scss', diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts index 46981a3ee..4f4cbc97d 100644 --- a/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-properties/resource-properties.component.ts @@ -69,7 +69,7 @@ export class ResourcePropertiesComponent { permissions = computed(() => { if (this.authService.restrictions().length > 0) { return { - canAddProperty: this.authService.hasPermission('RESOURCE', 'UPDATE', null, null, this.context().name), + canAddProperty: this.authService.hasPermission('RESOURCE', 'UPDATE', null, null, this.context()?.name), }; } else { return { canAddProperty: false }; diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html new file mode 100644 index 000000000..8ea197f00 --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.html @@ -0,0 +1,157 @@ + + + @if (!releases() || releases().length < 1) { +
+ No Release for this resource +
+ } @else { + + + @for (release of releases(); track release) { + + + + + } + +
+ {{ release.name }} + + @if (release.id === id() && permissions().canChangeRelease) { + + + + } + @if (permissions().canDeleteRelease) { + + + + } +
+ } +
+ + + + + + + + + + + + + + + + + + diff --git a/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts new file mode 100644 index 000000000..e0490736b --- /dev/null +++ b/AMW_angular/io/src/app/resources/resource-edit/resource-releases/resource-releases.component.ts @@ -0,0 +1,242 @@ +import { Component, computed, inject, input, signal, ViewChild, TemplateRef } from '@angular/core'; +import { LoadingIndicatorComponent } from '../../../shared/elements/loading-indicator.component'; +import { AuthService } from '../../../auth/auth.service'; +import { TileComponent } from '../../../shared/tile/tile.component'; +import { Release } from '../../models/release'; +import { RouterLink, Router, ActivatedRoute } from '@angular/router'; +import { IconComponent } from '../../../shared/icon/icon.component'; +import { ButtonComponent } from '../../../shared/button/button.component'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ResourceService } from '../../services/resource.service'; +import { Resource } from '../../models/resource'; +import { FormsModule } from '@angular/forms'; +import { ModalHeaderComponent } from '../../../shared/modal-header/modal-header.component'; +import { ToastService } from '../../../shared/elements/toast/toast.service'; + +@Component({ + selector: 'app-resource-releases', + standalone: true, + imports: [ + LoadingIndicatorComponent, + TileComponent, + RouterLink, + IconComponent, + ButtonComponent, + FormsModule, + ModalHeaderComponent, + ], + templateUrl: './resource-releases.component.html', +}) +export class ResourceReleasesComponent { + private authService = inject(AuthService); + private modalService = inject(NgbModal); + private resourceService = inject(ResourceService); + private router = inject(Router); + private route = inject(ActivatedRoute); + private toastService = inject(ToastService); + + isLoading = signal(false); + releaseToDelete = signal(null); + availableReleases = signal([]); + selectedReleaseId = signal(null); + isCreatingRelease = signal(false); + releaseToChange = signal(null); + isChangingRelease = signal(false); + + @ViewChild('createReleaseModal') createReleaseModal!: TemplateRef; + @ViewChild('changeReleaseModal') changeReleaseModal!: TemplateRef; + + id = input.required(); + releases = input.required(); + contextId = input.required(); + resource = input.required(); + + permissions = computed(() => { + if (this.authService.restrictions().length > 0) { + const resourceTypeName = this.resource()?.type ?? null; + const resourceGroupId = this.resource()?.resourceGroupId ?? null; + return { + canAddRelease: this.authService.hasPermission('RESOURCE', 'CREATE', resourceTypeName, resourceGroupId), + canChangeRelease: this.authService.hasPermission('RELEASE', 'UPDATE', resourceTypeName, resourceGroupId), + canDeleteRelease: this.authService.hasPermission('RESOURCE', 'DELETE', resourceTypeName, resourceGroupId), + }; + } + return { canAddRelease: false, canChangeRelease: false, canDeleteRelease: false }; + }); + + addRelease() { + this.isLoading.set(true); + this.resourceService.getAvailableReleasesForResource(this.id()).subscribe({ + next: (releases) => { + this.availableReleases.set(releases); + this.selectedReleaseId.set(null); + this.isLoading.set(false); + this.showCreateReleaseModal(); + }, + error: (error) => { + console.error('Failed to load available releases:', error); + this.toastService.error('Failed to load available releases.'); + this.isLoading.set(false); + }, + }); + } + + showCreateReleaseModal() { + const modalRef = this.modalService.open(this.createReleaseModal); + modalRef.result.then( + () => { + if (this.selectedReleaseId()) { + this.createRelease(this.selectedReleaseId()!); + } + }, + () => { + this.selectedReleaseId.set(null); + }, + ); + } + + createRelease(releaseId: number) { + const selectedRelease = this.availableReleases().find((r) => r.id === releaseId); + if (!selectedRelease || !selectedRelease.name) { + console.error('Selected release not found'); + return; + } + + const currentResource = this.resource(); + if (!currentResource || !currentResource.name) { + console.error('Current resource not found'); + return; + } + + const currentReleaseName = this.releases().find((r) => r.id === this.id())?.name; + if (!currentReleaseName) { + console.error('Current release name not found'); + return; + } + + this.isCreatingRelease.set(true); + this.resourceService + .createResourceRelease(currentResource.name, selectedRelease.name, currentReleaseName) + .subscribe({ + next: () => { + this.isCreatingRelease.set(false); + this.selectedReleaseId.set(null); + this.resourceService.setIdForResource(this.id()); + }, + error: (error) => { + console.error('Failed to create release:', error); + this.toastService.error('Failed to create release.'); + this.isCreatingRelease.set(false); + this.selectedReleaseId.set(null); + }, + }); + } + + showChangeReleaseModal(release: Release) { + this.releaseToChange.set(release); + this.isLoading.set(true); + this.resourceService.getAvailableReleasesForResource(release.id).subscribe({ + next: (releases) => { + this.availableReleases.set(releases); + this.selectedReleaseId.set(null); + this.isLoading.set(false); + this.openChangeReleaseModal(); + }, + error: (error) => { + console.error('Failed to load available releases:', error); + this.toastService.error('Failed to load available releases.'); + this.isLoading.set(false); + this.releaseToChange.set(null); + }, + }); + } + + openChangeReleaseModal() { + const modalRef = this.modalService.open(this.changeReleaseModal); + modalRef.result.then( + () => { + if (this.selectedReleaseId() && this.releaseToChange()) { + this.changeRelease(this.releaseToChange()!.id, this.selectedReleaseId()!); + } + }, + () => { + this.selectedReleaseId.set(null); + this.releaseToChange.set(null); + }, + ); + } + + changeRelease(resourceId: number, releaseId: number) { + this.isChangingRelease.set(true); + this.resourceService.changeResourceRelease(resourceId, releaseId).subscribe({ + next: () => { + this.isChangingRelease.set(false); + this.selectedReleaseId.set(null); + this.releaseToChange.set(null); + // Reload the resource and releases data to reflect the change + this.resourceService.setIdForResource(resourceId); + // Navigate to the changed resource (stays on the same resource, now in new release) + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { id: resourceId }, + queryParamsHandling: 'merge', + }); + }, + error: (error) => { + console.error('Failed to change release:', error); + this.toastService.error('Failed to change release.'); + this.isChangingRelease.set(false); + this.selectedReleaseId.set(null); + this.releaseToChange.set(null); + }, + }); + } + + showDeleteConfirmation(content: unknown, release: Release) { + this.releaseToDelete.set(release); + this.modalService.open(content).result.then( + () => { + this.deleteRelease(release.id); + }, + () => { + this.releaseToDelete.set(null); + }, + ); + } + + deleteRelease(resourceIdToDelete: number) { + // Note: release.id is actually the Resource ID, not the Release ID + // This is how the backend constructs the ReleaseDTO + this.isLoading.set(true); + this.resourceService.deleteResourceByResourceId(resourceIdToDelete).subscribe({ + next: () => { + this.isLoading.set(false); + this.releaseToDelete.set(null); + // Only navigate if we deleted the currently selected release + if (resourceIdToDelete === this.id()) { + const remainingReleases = this.releases().filter((r) => r.id !== resourceIdToDelete); + if (remainingReleases.length > 0) { + // Navigate to the first remaining release + void this.router.navigate([], { + relativeTo: this.route, + queryParams: { id: remainingReleases[0].id }, + queryParamsHandling: 'merge', + }); + } else { + // No releases left, navigate to resources list + void this.router.navigate(['/resources']); + } + } else { + // Reload the releases list to remove the deleted release from the UI + this.resourceService.setIdForResource(this.id()); + } + }, + error: (error) => { + console.error('Failed to delete release:', error); + this.toastService.error('Failed to delete release.'); + this.isLoading.set(false); + this.releaseToDelete.set(null); + }, + }); + } +} diff --git a/AMW_angular/io/src/app/resources/services/resource.service.ts b/AMW_angular/io/src/app/resources/services/resource.service.ts index 88c70c62d..353265eff 100644 --- a/AMW_angular/io/src/app/resources/services/resource.service.ts +++ b/AMW_angular/io/src/app/resources/services/resource.service.ts @@ -5,7 +5,6 @@ import { map, catchError, switchMap, shareReplay } from 'rxjs/operators'; import { Resource } from '../models/resource'; import { Release } from '../models/release'; import { Relation } from '../models/relation'; -import { Property } from '../models/property'; import { AppWithVersion } from '../../deployment/app-with-version'; import { BaseService } from '../../base/base.service'; import { ResourceType } from '../models/resource-type'; @@ -202,6 +201,40 @@ export class ResourceService extends BaseService { .get(`${this.getBaseUrl()}/resources/resourceGroups/releases/${resourceId}`) .pipe(catchError(this.handleError)); } + + deleteResourceByResourceId(resourceId: number): Observable { + return this.http + .delete(`${this.getBaseUrl()}/resources/${resourceId}`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + getAvailableReleasesForResource(resourceId: number): Observable { + return this.http + .get(`${this.getBaseUrl()}/resources/${resourceId}/availableReleases`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + createResourceRelease(resourceGroupName: string, releaseName: string, sourceReleaseName: string): Observable { + return this.http + .post( + `${this.getBaseUrl()}/resources/${resourceGroupName}`, + { releaseName, sourceReleaseName }, + { headers: this.getHeaders() }, + ) + .pipe(catchError(this.handleError)); + } + + changeResourceRelease(resourceId: number, releaseId: number): Observable { + return this.http + .put(`${this.getBaseUrl()}/resources/${resourceId}/release/${releaseId}`, null, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } } function toAppWithVersion(r: any): AppWithVersion { diff --git a/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts b/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts index 0da2c7f9c..9d2d1dd4e 100644 --- a/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts +++ b/AMW_angular/io/src/app/shared/modal-header/modal-header.component.ts @@ -2,6 +2,7 @@ import { Component, input, output } from '@angular/core'; @Component({ selector: 'app-modal-header', + standalone: true, imports: [], template: `