From 6549fa510efe5c5245a2abe9b8466f039f7a8126 Mon Sep 17 00:00:00 2001 From: StephGit <24672636+StephGit@users.noreply.github.com> Date: Tue, 6 Feb 2024 08:52:26 +0100 Subject: [PATCH] Feat/729 refactor settings releases (#733) * refactor: settings release component --- .../auditview-table.component.ts | 4 +- .../auditview-table.service.ts | 4 +- AMW_angular/io/src/app/auth/auth.service.ts | 49 +++++ AMW_angular/io/src/app/auth/restriction.ts | 8 + AMW_angular/io/src/app/core/amw-constants.ts | 3 +- .../deployments/deployments-list.component.ts | 4 +- .../releases/release-delete.component.html | 39 ++++ .../releases/release-delete.component.spec.ts | 16 ++ .../releases/release-delete.component.ts | 43 ++++ .../releases/release-edit.component.html | 43 ++++ .../releases/release-edit.component.spec.ts | 15 ++ .../releases/release-edit.component.ts | 60 +++++ .../io/src/app/settings/releases/release.ts | 9 + .../settings/releases/releases.component.html | 76 +++++++ .../releases/releases.component.spec.ts | 28 +++ .../settings/releases/releases.component.ts | 201 +++++++++++++++++ .../app/settings/releases/releases.service.ts | 73 ++++++ .../app/settings/releases/resourceEntity.ts | 5 + .../src/app/settings/settings.component.html | 2 +- .../io/src/app/settings/settings.routes.ts | 2 + .../date-picker/date-picker.component.html | 43 ++++ .../date-picker/date-picker.component.ts | 118 ++++++++++ .../app/shared/date-picker/date.model.spec.ts | 23 ++ .../src/app/shared/date-picker/date.model.ts | 74 +++++++ .../date-time-picker.component.ts | 4 +- .../date-time-picker/date-time.model.ts | 6 +- .../pagination/pagination.component.html | 10 +- AMW_angular/io/src/main.ts | 2 +- .../ResourceDependencyResolverService.java | 5 +- .../releasing/boundary/ReleaseLocator.java | 109 ++++++++- .../releasing/boundary/ResourceEntityDto.java | 30 +++ .../boundary/ResourceTypeEntityDto.java | 24 ++ .../ReleaseMgmtPersistenceService.java | 17 +- .../releasing/control/ReleaseMgmtService.java | 38 ---- .../releasing/entity/ReleaseEntity.java | 1 - .../boundary/ResourceLocator.java | 5 +- .../ConcurrentModificationException.java | 14 ++ .../itc/mobiliar/rest/RESTApplication.java | 1 + ...ConcurrentModificationExceptionMapper.java | 36 +++ .../mobiliar/rest/releases/ReleasesRest.java | 101 +++++++-- .../rest/resources/ResourceGroupsRest.java | 7 +- .../resources/ResourceGroupsRestTest.java | 6 +- .../release/ReleaseMgmtScreenService.java | 208 ------------------ .../pages/components/releasesScreenComp.xhtml | 201 ----------------- AMW_web/src/main/webapp/pages/settings.xhtml | 14 +- 45 files changed, 1267 insertions(+), 514 deletions(-) create mode 100644 AMW_angular/io/src/app/auth/auth.service.ts create mode 100644 AMW_angular/io/src/app/auth/restriction.ts create mode 100644 AMW_angular/io/src/app/settings/releases/release-delete.component.html create mode 100644 AMW_angular/io/src/app/settings/releases/release-delete.component.spec.ts create mode 100644 AMW_angular/io/src/app/settings/releases/release-delete.component.ts create mode 100644 AMW_angular/io/src/app/settings/releases/release-edit.component.html create mode 100644 AMW_angular/io/src/app/settings/releases/release-edit.component.spec.ts create mode 100644 AMW_angular/io/src/app/settings/releases/release-edit.component.ts create mode 100644 AMW_angular/io/src/app/settings/releases/release.ts create mode 100644 AMW_angular/io/src/app/settings/releases/releases.component.html create mode 100644 AMW_angular/io/src/app/settings/releases/releases.component.spec.ts create mode 100644 AMW_angular/io/src/app/settings/releases/releases.component.ts create mode 100644 AMW_angular/io/src/app/settings/releases/releases.service.ts create mode 100644 AMW_angular/io/src/app/settings/releases/resourceEntity.ts create mode 100644 AMW_angular/io/src/app/shared/date-picker/date-picker.component.html create mode 100644 AMW_angular/io/src/app/shared/date-picker/date-picker.component.ts create mode 100644 AMW_angular/io/src/app/shared/date-picker/date.model.spec.ts create mode 100644 AMW_angular/io/src/app/shared/date-picker/date.model.ts create mode 100644 AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceEntityDto.java create mode 100644 AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceTypeEntityDto.java create mode 100644 AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/exception/ConcurrentModificationException.java create mode 100644 AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/exceptions/ConcurrentModificationExceptionMapper.java delete mode 100644 AMW_web/src/main/java/ch/puzzle/itc/mobiliar/presentation/release/ReleaseMgmtScreenService.java delete mode 100644 AMW_web/src/main/webapp/pages/components/releasesScreenComp.xhtml diff --git a/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.component.ts b/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.component.ts index 40f063aa6..3e87139fb 100644 --- a/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.component.ts +++ b/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.component.ts @@ -3,7 +3,7 @@ import { Observable } from 'rxjs'; import { AuditLogEntry } from '../auditview-entry'; import { AuditviewTableService } from './auditview-table.service'; import { SortableHeader, SortEvent } from './sortable.directive'; -import { DATE_FORMAT } from '../../core/amw-constants'; +import { DATE_TIME_FORMAT } from '../../core/amw-constants'; import { NewlineFilterPipe } from './newlineFilterPipe'; import { NgbHighlight } from '@ng-bootstrap/ng-bootstrap'; import { AsyncPipe, DatePipe, NgFor } from '@angular/common'; @@ -22,7 +22,7 @@ export class AuditviewTableComponent implements OnChanges { auditlogEntries$: Observable; @ViewChildren(SortableHeader) headers: QueryList; - dateFormat = DATE_FORMAT; + dateFormat = DATE_TIME_FORMAT; constructor(public service: AuditviewTableService) { this.auditlogEntries$ = service.result$; diff --git a/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.service.ts b/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.service.ts index d919f52af..5a6c096b5 100644 --- a/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.service.ts +++ b/AMW_angular/io/src/app/auditview/auditview-table/auditview-table.service.ts @@ -4,7 +4,7 @@ import { debounceTime, map, tap } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; import { AuditLogEntry } from '../auditview-entry'; import { SortDirection } from './sortable.directive'; -import { DATE_FORMAT } from '../../core/amw-constants'; +import { DATE_TIME_FORMAT } from '../../core/amw-constants'; interface State { searchTerm: string; @@ -26,7 +26,7 @@ function sort(entries: AuditLogEntry[], column: string, direction: string): Audi function matches(entry: AuditLogEntry, term: string, pipe: DatePipe): boolean { const lowerCaseTerm = term.toLowerCase(); return ( - pipe.transform(entry.timestamp, DATE_FORMAT).includes(term) || + pipe.transform(entry.timestamp, DATE_TIME_FORMAT).includes(term) || nullSafeToLowerCase(entry.mode).includes(lowerCaseTerm) || nullSafeToLowerCase(entry.editContextName).includes(lowerCaseTerm) || nullSafeToLowerCase(entry.name).includes(lowerCaseTerm) || diff --git a/AMW_angular/io/src/app/auth/auth.service.ts b/AMW_angular/io/src/app/auth/auth.service.ts new file mode 100644 index 000000000..03ce3424f --- /dev/null +++ b/AMW_angular/io/src/app/auth/auth.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { BaseService } from '../base/base.service'; +import { HttpClient } from '@angular/common/http'; +import { filter, Observable, Subject } from 'rxjs'; +import { catchError, map } from 'rxjs/operators'; +import { Restriction } from '../settings/permission/restriction'; +import { Release } from '../settings/releases/release'; + +@Injectable({ providedIn: 'root' }) +export class AuthService extends BaseService { + private readonly _cachedUserRestrictions: Subject; + private readonly _userData: Observable; + + constructor(private http: HttpClient) { + super(); + if (!this._cachedUserRestrictions) { + this._cachedUserRestrictions = new Subject(); + this.refreshData(); + } + this._userData = this._cachedUserRestrictions.asObservable(); + } + + private refreshData() { + this.getRestrictions().subscribe({ + next: (r) => this._cachedUserRestrictions.next(r), + error: (e) => console.log(e), + }); + } + + private getRestrictions(): Observable { + return this.http + .get(`${this.getBaseUrl()}/permissions/restrictions/ownRestrictions/`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + get userRestrictions() { + return this._userData; + } + + getActionsForPermission(permissionName: string) { + return this._userData.pipe( + map((restrictions) => { + return restrictions.filter((entry) => entry.permission.name === permissionName).map((entry) => entry.action); + }), + ); + } +} diff --git a/AMW_angular/io/src/app/auth/restriction.ts b/AMW_angular/io/src/app/auth/restriction.ts new file mode 100644 index 000000000..e0f120d29 --- /dev/null +++ b/AMW_angular/io/src/app/auth/restriction.ts @@ -0,0 +1,8 @@ +export interface Restriction { + role: string; + permission: string; + resourceType: string; + resourceTypePermission: string; + context: number; + action: boolean; +} diff --git a/AMW_angular/io/src/app/core/amw-constants.ts b/AMW_angular/io/src/app/core/amw-constants.ts index 1207d692a..12697a286 100644 --- a/AMW_angular/io/src/app/core/amw-constants.ts +++ b/AMW_angular/io/src/app/core/amw-constants.ts @@ -1,3 +1,4 @@ export const AMW_LOGOUT_URL = 'amw.logoutUrl'; // used for date-fns -export const DATE_FORMAT = 'dd.MM.yyyy HH:mm'; +export const DATE_TIME_FORMAT = 'dd.MM.yyyy HH:mm'; +export const DATE_FORMAT = 'dd.MM.yyyy'; diff --git a/AMW_angular/io/src/app/deployments/deployments-list.component.ts b/AMW_angular/io/src/app/deployments/deployments-list.component.ts index e52f1e242..5f10e7bce 100644 --- a/AMW_angular/io/src/app/deployments/deployments-list.component.ts +++ b/AMW_angular/io/src/app/deployments/deployments-list.component.ts @@ -5,7 +5,7 @@ import { ResourceService } from '../resource/resource.service'; import * as _ from 'lodash'; import { DateTimeModel } from '../shared/date-time-picker/date-time.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { DATE_FORMAT } from '../core/amw-constants'; +import { DATE_TIME_FORMAT } from '../core/amw-constants'; import { DateTimePickerComponent } from '../shared/date-time-picker/date-time-picker.component'; import { RouterLink } from '@angular/router'; import { IconComponent } from '../shared/icon/icon.component'; @@ -46,7 +46,7 @@ export class DeploymentsListComponent { allSelected: boolean = false; // TODO: show this error somewhere? errorMessage = ''; - dateFormat = DATE_FORMAT; + dateFormat = DATE_TIME_FORMAT; failureReason: { [key: string]: string } = { PRE_DEPLOYMENT_GENERATION: 'pre deployment generation failed', diff --git a/AMW_angular/io/src/app/settings/releases/release-delete.component.html b/AMW_angular/io/src/app/settings/releases/release-delete.component.html new file mode 100644 index 000000000..4e655af15 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release-delete.component.html @@ -0,0 +1,39 @@ + + + diff --git a/AMW_angular/io/src/app/settings/releases/release-delete.component.spec.ts b/AMW_angular/io/src/app/settings/releases/release-delete.component.spec.ts new file mode 100644 index 000000000..2302337d1 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release-delete.component.spec.ts @@ -0,0 +1,16 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ReleaseDeleteComponent } from './release-delete.component'; + +describe('ReleaseDeleteComponent', () => { + let component: ReleaseDeleteComponent; + const activeModal = new NgbActiveModal(); + + beforeEach(async () => { + component = new ReleaseDeleteComponent(activeModal); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/settings/releases/release-delete.component.ts b/AMW_angular/io/src/app/settings/releases/release-delete.component.ts new file mode 100644 index 000000000..062f68044 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release-delete.component.ts @@ -0,0 +1,43 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Release } from './release'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { AsyncPipe, KeyValuePipe, NgFor, NgForOf, NgIf } from '@angular/common'; +import { ResourceEntity } from './resourceEntity'; + +@Component({ + selector: 'amw-release-delete', + standalone: true, + imports: [AsyncPipe, KeyValuePipe, NgIf, NgFor, NgForOf, FormsModule], + templateUrl: './release-delete.component.html', +}) +export class ReleaseDeleteComponent implements OnInit { + @Input() release: Release; + @Input() resources: Map; + @Output() deleteRelease: EventEmitter = new EventEmitter(); + + hasResources: boolean = false; + + constructor(public activeModal: NgbActiveModal) { + this.activeModal = activeModal; + } + + ngOnInit(): void { + if (this.resources.size > 0) { + this.hasResources = true; + } + } + + getTitle(): string { + return 'Remove release'; + } + + cancel() { + this.activeModal.close(); + } + + delete() { + this.deleteRelease.emit(this.release); + this.activeModal.close(); + } +} diff --git a/AMW_angular/io/src/app/settings/releases/release-edit.component.html b/AMW_angular/io/src/app/settings/releases/release-edit.component.html new file mode 100644 index 000000000..3143c8c73 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release-edit.component.html @@ -0,0 +1,43 @@ + + + diff --git a/AMW_angular/io/src/app/settings/releases/release-edit.component.spec.ts b/AMW_angular/io/src/app/settings/releases/release-edit.component.spec.ts new file mode 100644 index 000000000..bd71c600c --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release-edit.component.spec.ts @@ -0,0 +1,15 @@ +import { ReleaseEditComponent } from './release-edit.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('ReleasesEditComponent', () => { + let component: ReleaseEditComponent; + const activeModal = new NgbActiveModal(); + + beforeEach(async () => { + component = new ReleaseEditComponent(activeModal); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/settings/releases/release-edit.component.ts b/AMW_angular/io/src/app/settings/releases/release-edit.component.ts new file mode 100644 index 000000000..793c093c1 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release-edit.component.ts @@ -0,0 +1,60 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { Release } from './release'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { DatePickerComponent } from '../../shared/date-picker/date-picker.component'; +import { DATE_FORMAT } from '../../core/amw-constants'; +import { DateModel } from '../../shared/date-picker/date.model'; +import { NgIf } from '@angular/common'; + +@Component({ + selector: 'amw-release-edit', + templateUrl: './release-edit.component.html', + standalone: true, + imports: [DatePickerComponent, NgIf, FormsModule], +}) +export class ReleaseEditComponent implements OnInit { + @Input() release: Release; + @Output() saveRelease: EventEmitter = new EventEmitter(); + + dateFormat = DATE_FORMAT; + installationDate: DateModel = null; + + constructor(public activeModal: NgbActiveModal) { + this.activeModal = activeModal; + } + + ngOnInit(): void { + if (this.release) { + this.installationDate = DateModel.fromEpoch(this.release.installationInProductionAt); + } + } + + getTitle(): string { + return this.release.id ? 'Edit release' : 'Add release'; + } + + hasInvalidDate(): boolean { + return this.installationDate == null || this.installationDate.toEpoch() == null; + } + + cancel() { + this.activeModal.close(); + } + + save() { + if (this.installationDate.toEpoch() != null) { + const release: Release = { + name: this.release.name, + mainRelease: this.release.mainRelease, + description: this.release.description, + installationInProductionAt: this.installationDate.toEpoch(), + id: this.release.id ? this.release.id : null, + default: false, + v: this.release.v ? this.release.v : null, + }; + this.saveRelease.emit(release); + this.activeModal.close(); + } + } +} diff --git a/AMW_angular/io/src/app/settings/releases/release.ts b/AMW_angular/io/src/app/settings/releases/release.ts new file mode 100644 index 000000000..9cb5b8631 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/release.ts @@ -0,0 +1,9 @@ +export interface Release { + name: string; + mainRelease: boolean; + description: string; + installationInProductionAt: number; + id: number; + v: number; + default: boolean; +} diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.html b/AMW_angular/io/src/app/settings/releases/releases.component.html new file mode 100644 index 000000000..fbc9e8df8 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/releases.component.html @@ -0,0 +1,76 @@ + + +
+

Releases

+
+
+
+
+ Releases +
+
+ @if (canCreate) { + + } +
+
+
+
+
+ @if ((results$ | async)?.length > 0) { + + + + + + + + + + + + + + @for (item of results$ | async; track item) { + + + + + + + + + } + +
Release NameMain ReleaseDescriptionDateEditDelete
{{ item.name }}{{ item.mainRelease }}{{ item.description }}{{ item.installationInProductionAt | date: dateFormat }} + @if (item.default != true && canEdit) { + + } + + @if (item.default != true && canDelete) { + + } +
+ } +
+
+ +
+
diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.spec.ts b/AMW_angular/io/src/app/settings/releases/releases.component.spec.ts new file mode 100644 index 000000000..079f8c5e2 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/releases.component.spec.ts @@ -0,0 +1,28 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReleasesComponent } from './releases.component'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; +import { HttpClient } from '@angular/common/http'; + +describe('ReleasesComponent', () => { + let component: ReleasesComponent; + let fixture: ComponentFixture; + let httpTestingController: HttpTestingController; + let httpClient: HttpClient; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ReleasesComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ReleasesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + httpTestingController = TestBed.inject(HttpTestingController); + httpClient = TestBed.inject(HttpClient); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/AMW_angular/io/src/app/settings/releases/releases.component.ts b/AMW_angular/io/src/app/settings/releases/releases.component.ts new file mode 100644 index 000000000..411fa8959 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/releases.component.ts @@ -0,0 +1,201 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { AsyncPipe, DatePipe, NgFor, NgIf } from '@angular/common'; +import { LoadingIndicatorComponent } from '../../shared/elements/loading-indicator.component'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { IconComponent } from '../../shared/icon/icon.component'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; +import { ToastComponent } from 'src/app/shared/elements/toast/toast.component'; +import { DATE_FORMAT } from '../../core/amw-constants'; +import { ReleaseEditComponent } from './release-edit.component'; +import { Release } from './release'; +import { ReleasesService } from './releases.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { AuthService } from '../../auth/auth.service'; +import { combineLatest } from 'rxjs'; +import { map, takeUntil } from 'rxjs/operators'; +import { ReleaseDeleteComponent } from './release-delete.component'; + +@Component({ + selector: 'amw-releases', + standalone: true, + imports: [ + AsyncPipe, + DatePipe, + IconComponent, + LoadingIndicatorComponent, + NgIf, + NgFor, + PaginationComponent, + ReleaseEditComponent, + ReleaseDeleteComponent, + ToastComponent, + ], + providers: [AuthService], + templateUrl: './releases.component.html', +}) +export class ReleasesComponent implements OnInit { + releases$: Observable; + defaultRelease$: Observable; + count$: Observable; + results$: Observable; + private error$ = new BehaviorSubject(''); + + private destroy$ = new Subject(); + + dateFormat = DATE_FORMAT; + + // pagination with default values + maxResults: number = 10; + offset: number = 0; + allResults: number; + currentPage: number; + lastPage: number; + + isLoading: boolean = true; + + canCreate: boolean = false; + canEdit: boolean = false; + canDelete: boolean = false; + + @ViewChild(ToastComponent) toast: ToastComponent; + + constructor( + private authService: AuthService, + private http: HttpClient, + private modalService: NgbModal, + private releasesService: ReleasesService, + ) {} + + ngOnInit(): void { + this.error$.pipe(takeUntil(this.destroy$)).subscribe((msg) => { + msg != '' ? this.toast.display(msg, 'error', 5000) : null; + }); + this.getUserPermissions(); + this.count$ = this.releasesService.getCount(); + this.defaultRelease$ = this.releasesService.getDefaultRelease(); + this.getReleases(); + } + + ngOnDestroy(): void { + this.destroy$.next(undefined); + } + + private getUserPermissions() { + this.authService + .getActionsForPermission('RELEASE') + .pipe(takeUntil(this.destroy$)) + .subscribe((value) => { + if (value.indexOf('ALL') > -1) { + this.canDelete = this.canEdit = this.canCreate = true; + } else { + this.canCreate = value.indexOf('CREATE') > -1; + this.canEdit = value.indexOf('UPDATE') > -1; + this.canDelete = value.indexOf('DELETE') > -1; + } + }); + } + + private getReleases() { + this.isLoading = true; + this.releases$ = this.releasesService.getReleases(this.offset, this.maxResults); + this.currentPage = Math.floor(this.offset / this.maxResults) + 1; + + this.results$ = combineLatest([this.releases$, this.defaultRelease$, this.count$]) + .pipe( + map(([releases, defaultR, count]) => { + this.lastPage = Math.ceil(count / this.maxResults); + releases.map((release) => { + if (release.id === defaultR.id) { + release.default = true; + } + return release; + }); + return releases; + }), + ) + .pipe(); + + this.isLoading = false; + } + + addRelease() { + const modalRef = this.modalService.open(ReleaseEditComponent); + modalRef.componentInstance.release = { + default: false, + description: '', + id: 0, + installationInProductionAt: undefined, + mainRelease: false, + name: '', + }; + modalRef.componentInstance.saveRelease + .pipe(takeUntil(this.destroy$)) + .subscribe((release: Release) => this.save(release)); + } + + editRelease(release: Release) { + const modalRef = this.modalService.open(ReleaseEditComponent); + modalRef.componentInstance.release = release; + modalRef.componentInstance.saveRelease + .pipe(takeUntil(this.destroy$)) + .subscribe((release: Release) => this.save(release)); + } + + save(release: Release) { + this.isLoading = true; + this.releasesService + .save(release) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (r) => r, + error: (e) => this.error$.next(e), + complete: () => { + this.toast.display('Release saved successfully.', 'success'); + this.getReleases(); + }, + }); + this.isLoading = false; + } + + deleteRelease(release: Release) { + this.releasesService + .getReleaseResources(release.id) + .pipe(takeUntil(this.destroy$)) + .subscribe((list) => { + const modalRef = this.modalService.open(ReleaseDeleteComponent); + modalRef.componentInstance.release = release; + modalRef.componentInstance.resources = list; + modalRef.componentInstance.deleteRelease + .pipe(takeUntil(this.destroy$)) + .subscribe((release: Release) => this.delete(release)); + }); + } + + delete(release: Release) { + this.isLoading = true; + this.releasesService + .delete(release.id) + .pipe(takeUntil(this.destroy$)) + .subscribe({ + next: (r) => r, + error: (e) => this.error$.next(e), + complete: () => { + this.toast.display('Release deleted.', 'success'); + this.getReleases(); + }, + }); + this.isLoading = false; + } + + setMaxResultsPerPage(max: number) { + this.maxResults = max; + this.offset = 0; + this.getReleases(); + } + + setNewOffset(offset: number) { + this.offset = offset; + this.getReleases(); + } +} diff --git a/AMW_angular/io/src/app/settings/releases/releases.service.ts b/AMW_angular/io/src/app/settings/releases/releases.service.ts new file mode 100644 index 000000000..f20ffecbd --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/releases.service.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@angular/core'; +import { BaseService } from '../../base/base.service'; +import { HttpClient } from '@angular/common/http'; +import { map, catchError } from 'rxjs/operators'; +import { Release } from './release'; +import { Observable } from 'rxjs'; +import { ResourceEntity } from './resourceEntity'; + +@Injectable({ providedIn: 'root' }) +export class ReleasesService extends BaseService { + constructor(private http: HttpClient) { + super(); + } + + getCount(): Observable { + return this.http.get(`${this.getBaseUrl()}/releases/count`).pipe(catchError(this.handleError)); + } + + getDefaultRelease(): Observable { + return this.http.get(`${this.getBaseUrl()}/releases/default`).pipe(catchError(this.handleError)); + } + + getReleases(offset: number, limit: number): Observable { + return this.http + .get(`${this.getBaseUrl()}/releases?start=${offset}&limit=${limit}`) + .pipe(catchError(this.handleError)); + } + + getReleaseResources(id: number): Observable> { + return this.http.get(`${this.getBaseUrl()}/releases/${id}/resources`).pipe( + map((jsonObject) => { + let resourceMap = new Map(); + for (var value in jsonObject) { + resourceMap.set(value, jsonObject[value]); + } + return resourceMap; + }), + catchError(this.handleError), + ); + } + + save(release: Release) { + if (release.id) { + return this.update(release); + } else { + return this.create(release); + } + } + + private create(release: Release) { + return this.http + .post(`${this.getBaseUrl()}/releases`, release, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + private update(release: Release) { + return this.http + .put(`${this.getBaseUrl()}/releases/${release.id}`, release, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } + + delete(id: number) { + return this.http + .delete(`${this.getBaseUrl()}/releases/${id}`, { + headers: this.getHeaders(), + }) + .pipe(catchError(this.handleError)); + } +} diff --git a/AMW_angular/io/src/app/settings/releases/resourceEntity.ts b/AMW_angular/io/src/app/settings/releases/resourceEntity.ts new file mode 100644 index 000000000..4b4d87f45 --- /dev/null +++ b/AMW_angular/io/src/app/settings/releases/resourceEntity.ts @@ -0,0 +1,5 @@ +export interface ResourceEntity { + id: number; + name: string; + resourceType: {}; +} diff --git a/AMW_angular/io/src/app/settings/settings.component.html b/AMW_angular/io/src/app/settings/settings.component.html index 8ba27ab04..62b0de8f2 100644 --- a/AMW_angular/io/src/app/settings/settings.component.html +++ b/AMW_angular/io/src/app/settings/settings.component.html @@ -10,7 +10,7 @@ Deployment Parameter My favorites STP Management - Releases + Releases Roles and Permissions Application Info diff --git a/AMW_angular/io/src/app/settings/settings.routes.ts b/AMW_angular/io/src/app/settings/settings.routes.ts index 6aa4641f3..129e81107 100644 --- a/AMW_angular/io/src/app/settings/settings.routes.ts +++ b/AMW_angular/io/src/app/settings/settings.routes.ts @@ -2,6 +2,7 @@ import { TagsComponent } from './tags/tags.component'; import { SettingsComponent } from './settings.component'; import { PermissionComponent } from './permission/permission.component'; import { ApplicationInfoComponent } from './application-info/application-info.component'; +import { ReleasesComponent } from './releases/releases.component'; export const settingsRoutes = [ { @@ -9,6 +10,7 @@ export const settingsRoutes = [ component: SettingsComponent, title: 'Settings', children: [ + { path: 'releases', component: ReleasesComponent }, { title: 'Settings - Tags', path: 'tags', component: TagsComponent }, { path: 'permission/delegation/:actingUser', diff --git a/AMW_angular/io/src/app/shared/date-picker/date-picker.component.html b/AMW_angular/io/src/app/shared/date-picker/date-picker.component.html new file mode 100644 index 000000000..295ce7d81 --- /dev/null +++ b/AMW_angular/io/src/app/shared/date-picker/date-picker.component.html @@ -0,0 +1,43 @@ +@if (errorMessage) { + +} +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ +
+ @if (false) { +
+ } @else { +
+ } +
diff --git a/AMW_angular/io/src/app/shared/date-picker/date-picker.component.ts b/AMW_angular/io/src/app/shared/date-picker/date-picker.component.ts new file mode 100644 index 000000000..7502c5f58 --- /dev/null +++ b/AMW_angular/io/src/app/shared/date-picker/date-picker.component.ts @@ -0,0 +1,118 @@ +import { Component, OnInit, Input, forwardRef, ViewChild, Injector } from '@angular/core'; +import { NgbPopoverConfig, NgbPopover, NgbDateStruct, NgbDatepicker } from '@ng-bootstrap/ng-bootstrap'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor, NgControl, FormsModule } from '@angular/forms'; +import { DatePipe, NgIf, NgClass } from '@angular/common'; +import { DateModel } from './date.model'; +import { noop } from 'rxjs'; +import { DATE_FORMAT } from 'src/app/core/amw-constants'; +import { IconComponent } from '../icon/icon.component'; + +@Component({ + selector: 'app-date-picker', + templateUrl: './date-picker.component.html', + providers: [ + DatePipe, + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => DatePickerComponent), + multi: true, + }, + ], + standalone: true, + imports: [NgIf, FormsModule, NgClass, NgbPopover, IconComponent, NgbDatepicker], +}) +export class DatePickerComponent implements ControlValueAccessor, OnInit { + @Input() + dateStringFormat = DATE_FORMAT; + @Input() + disabled = false; + + dateString = ''; + + date = new DateModel(); + errorMessage = ''; + + @ViewChild(NgbPopover) + popover: NgbPopover; + + onTouched: () => void = noop; + onChange: (_: any) => void = noop; + + ngControl: NgControl; + + constructor( + private config: NgbPopoverConfig, + private inj: Injector, + ) { + config.autoClose = 'outside'; + config.placement = 'auto'; + } + + ngOnInit(): void { + this.ngControl = this.inj.get(NgControl); + } + + writeValue(newModel: DateModel) { + if (newModel) { + this.date = Object.assign(this.date, newModel); + //this.datetime = newModel; + this.setDateString(); + } else { + this.date = new DateModel(); + } + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + toggleDateTimeState($event) { + $event.stopPropagation(); + this.popover.close(true); + } + + setDisabledState?(isDisabled: boolean): void { + this.disabled = isDisabled; + } + + // called when user updates the datestring directly + onDateStringChange($event: any) { + const value = $event.target.value; + const dt = DateModel.fromLocalString(value, this.dateStringFormat); + this.errorMessage = ''; + + if (dt) { + this.date = dt; + this.onChange(dt); + } else if (value.trim() === '') { + this.date = new DateModel(); + this.dateString = ''; + this.onChange(this.date); + } else { + this.errorMessage = 'Invalid date!'; + } + } + + onDateChange(event: NgbDateStruct) { + if (!event) { + return; + } + this.date.year = event.year; + this.date.month = event.month; + this.date.day = event.day; + this.setDateString(); + } + + setDateString() { + this.dateString = this.date.toString(this.dateStringFormat); + this.onChange(this.date); + } + + inputBlur($event) { + this.onTouched(); + } +} diff --git a/AMW_angular/io/src/app/shared/date-picker/date.model.spec.ts b/AMW_angular/io/src/app/shared/date-picker/date.model.spec.ts new file mode 100644 index 000000000..d3aefdb40 --- /dev/null +++ b/AMW_angular/io/src/app/shared/date-picker/date.model.spec.ts @@ -0,0 +1,23 @@ +import { TestBed } from '@angular/core/testing'; +import { DatePickerComponent } from './date-picker.component'; +import { DateModel } from './date.model'; +import * as datefns from 'date-fns'; + +describe('DateModel', () => { + it('should convert dates correctly', () => { + const fixture = TestBed.createComponent(DatePickerComponent); + const component = fixture.componentInstance; + const testDate = '02.01.2017'; + + const m = datefns.parse(testDate, component.dateStringFormat, new Date()); + const dateTimeFromString = DateModel.fromLocalString(testDate, component.dateStringFormat); + const dateTimeFromEpoch = DateModel.fromEpoch(m.getTime()); + + expect(dateTimeFromString.day).toEqual(m.getDate()); + expect(dateTimeFromString.month).toEqual(m.getMonth() + 1); + + expect(dateTimeFromString).toEqual(dateTimeFromEpoch); + + expect(dateTimeFromString.toEpoch()).toEqual(m.getTime()); + }); +}); diff --git a/AMW_angular/io/src/app/shared/date-picker/date.model.ts b/AMW_angular/io/src/app/shared/date-picker/date.model.ts new file mode 100644 index 000000000..b0e2aca5b --- /dev/null +++ b/AMW_angular/io/src/app/shared/date-picker/date.model.ts @@ -0,0 +1,74 @@ +import { NgbTimeStruct, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; +import * as datefns from 'date-fns'; +import { DATE_FORMAT } from 'src/app/core/amw-constants'; + +export interface NgbDateTimeStruct extends NgbDateStruct, NgbTimeStruct {} + +export class DateModel implements NgbDateStruct { + year: number; + month: number; + day: number; + + timeZoneOffset: number; + + public constructor(init?: Partial) { + Object.assign(this, init); + } + + private static fromDate(date: Date): DateModel { + if (!datefns.isValid(date)) { + return null; + } + return new DateModel({ + year: date.getFullYear(), + // months start at 0 + month: date.getMonth() + 1, + // getDate is the day of month + // getDay is the day of the week + day: date.getDate(), + timeZoneOffset: date.getTimezoneOffset(), + }); + } + + public static fromLocalString(dateString: string, format?: string): DateModel { + let date: Date; + if (typeof format === 'undefined') { + date = datefns.parse(dateString, DATE_FORMAT, new Date()); + } else { + date = datefns.parse(dateString, format, new Date()); + } + return this.fromDate(date); + } + + public static fromEpoch(epoch: number) { + const date = datefns.toDate(epoch); + return this.fromDate(date); + } + + private thisToDate(): Date { + return new Date(this.year, this.month - 1, this.day); + } + + public toString(format?: string): string { + const date = datefns.toDate(this.thisToDate()); + if (!datefns.isValid(date)) { + return null; + } + if (typeof format === 'undefined') { + return datefns.format(date, DATE_FORMAT); + } + return datefns.format(date, format); + } + + public toEpoch(): number { + const date = datefns.toDate(this.thisToDate()); + if (!datefns.isValid(date)) { + return null; + } + return date.getTime(); + } + + public toJSON(): string { + return this.toString(); + } +} diff --git a/AMW_angular/io/src/app/shared/date-time-picker/date-time-picker.component.ts b/AMW_angular/io/src/app/shared/date-time-picker/date-time-picker.component.ts index 2ca041c00..5ffe0b706 100644 --- a/AMW_angular/io/src/app/shared/date-time-picker/date-time-picker.component.ts +++ b/AMW_angular/io/src/app/shared/date-time-picker/date-time-picker.component.ts @@ -11,7 +11,7 @@ import { NG_VALUE_ACCESSOR, ControlValueAccessor, NgControl, FormsModule } from import { DatePipe, NgIf, NgClass } from '@angular/common'; import { DateTimeModel } from './date-time.model'; import { noop } from 'rxjs'; -import { DATE_FORMAT } from 'src/app/core/amw-constants'; +import { DATE_TIME_FORMAT } from 'src/app/core/amw-constants'; import { IconComponent } from '../icon/icon.component'; @Component({ @@ -31,7 +31,7 @@ import { IconComponent } from '../icon/icon.component'; }) export class DateTimePickerComponent implements ControlValueAccessor, OnInit, AfterViewInit { @Input() - dateStringFormat = DATE_FORMAT; + dateStringFormat = DATE_TIME_FORMAT; @Input() hourStep = 1; @Input() diff --git a/AMW_angular/io/src/app/shared/date-time-picker/date-time.model.ts b/AMW_angular/io/src/app/shared/date-time-picker/date-time.model.ts index 3505c10e0..9d1632c92 100644 --- a/AMW_angular/io/src/app/shared/date-time-picker/date-time.model.ts +++ b/AMW_angular/io/src/app/shared/date-time-picker/date-time.model.ts @@ -1,6 +1,6 @@ import { NgbTimeStruct, NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; import * as datefns from 'date-fns'; -import { DATE_FORMAT } from 'src/app/core/amw-constants'; +import { DATE_TIME_FORMAT } from 'src/app/core/amw-constants'; export interface NgbDateTimeStruct extends NgbDateStruct, NgbTimeStruct {} @@ -39,7 +39,7 @@ export class DateTimeModel implements NgbDateTimeStruct { public static fromLocalString(dateString: string, format?: string): DateTimeModel { let date: Date; if (typeof format === 'undefined') { - date = datefns.parse(dateString, DATE_FORMAT, new Date()); + date = datefns.parse(dateString, DATE_TIME_FORMAT, new Date()); } else { date = datefns.parse(dateString, format, new Date()); } @@ -61,7 +61,7 @@ export class DateTimeModel implements NgbDateTimeStruct { return null; } if (typeof format === 'undefined') { - return datefns.format(date, DATE_FORMAT); + return datefns.format(date, DATE_TIME_FORMAT); } return datefns.format(date, format); } diff --git a/AMW_angular/io/src/app/shared/pagination/pagination.component.html b/AMW_angular/io/src/app/shared/pagination/pagination.component.html index a5a18c659..9aeced220 100644 --- a/AMW_angular/io/src/app/shared/pagination/pagination.component.html +++ b/AMW_angular/io/src/app/shared/pagination/pagination.component.html @@ -4,19 +4,19 @@ diff --git a/AMW_angular/io/src/main.ts b/AMW_angular/io/src/main.ts index 42003a40a..6f6119a96 100644 --- a/AMW_angular/io/src/main.ts +++ b/AMW_angular/io/src/main.ts @@ -1,4 +1,4 @@ -import { enableProdMode, importProvidersFrom } from '@angular/core'; +import { enableProdMode } from '@angular/core'; import { environment } from './environments/environment'; import { AppComponent } from './app/app.component'; diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/generator/control/extracted/ResourceDependencyResolverService.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/generator/control/extracted/ResourceDependencyResolverService.java index 2cc340f9a..186135988 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/generator/control/extracted/ResourceDependencyResolverService.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/generator/control/extracted/ResourceDependencyResolverService.java @@ -28,6 +28,7 @@ import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceGroupEntity; import ch.puzzle.itc.mobiliar.business.resourcerelation.entity.ConsumedResourceRelationEntity; import ch.puzzle.itc.mobiliar.business.resourcerelation.entity.ProvidedResourceRelationEntity; +import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; import ch.puzzle.itc.mobiliar.common.util.DefaultResourceTypeDefinition; import javax.ejb.Stateless; @@ -233,7 +234,7 @@ public ResourceEntity getResourceEntityForRelease(@NotNull ResourceGroupEntity r * @param releaseId * @return */ - public ResourceEntity getResourceEntityForRelease(@NotNull Integer resourceGroupId, @NotNull Integer releaseId) { + public ResourceEntity getResourceEntityForRelease(@NotNull Integer resourceGroupId, @NotNull Integer releaseId) throws NotFoundException { ResourceGroupEntity resourceGroup = resourceGroupLocator.getResourceGroupForCreateDeploy(resourceGroupId); return getResourceEntityForRelease(resourceGroup.getResources(), releaseLocator.getReleaseById(releaseId)); } @@ -250,7 +251,7 @@ public ResourceEntity getResourceEntityForRelease(@NotNull Collection 0) { - //If the release date of the current resource is later than the the best release we've found yet, it is better suited and is our new "best resource" + //If the release date of the current resource is later than the best release we've found yet, it is better suited and is our new "best resource" bestResource = resource; } } diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ReleaseLocator.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ReleaseLocator.java index d5ce8e569..55bbdf33f 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ReleaseLocator.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ReleaseLocator.java @@ -20,19 +20,24 @@ package ch.puzzle.itc.mobiliar.business.releasing.boundary; -import java.util.List; -import java.util.logging.Logger; - -import javax.ejb.Stateless; -import javax.inject.Inject; - +import ch.puzzle.itc.mobiliar.business.releasing.control.ReleaseMgmtPersistenceService; import ch.puzzle.itc.mobiliar.business.releasing.control.ReleaseRepository; import ch.puzzle.itc.mobiliar.business.releasing.entity.ReleaseEntity; +import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceEntity; import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceGroupEntity; import ch.puzzle.itc.mobiliar.business.security.entity.Permission; import ch.puzzle.itc.mobiliar.business.security.interceptor.HasPermission; +import ch.puzzle.itc.mobiliar.common.exception.ConcurrentModificationException; +import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; +import lombok.NonNull; + +import javax.ejb.EJBTransactionRolledbackException; +import javax.ejb.Stateless; +import javax.inject.Inject; +import java.util.*; +import java.util.logging.Logger; -import static ch.puzzle.itc.mobiliar.business.security.entity.Action.DELETE; +import static ch.puzzle.itc.mobiliar.business.security.entity.Action.*; @Stateless public class ReleaseLocator { @@ -44,20 +49,106 @@ public class ReleaseLocator { @Inject ReleaseRepository releaseRepository; + @Inject + ReleaseMgmtPersistenceService persistenceService; + public ReleaseEntity getReleaseByName(String name) { return releaseRepository.getReleaseByName(name); } - public ReleaseEntity getReleaseById(Integer id) { - return releaseRepository.find(id); + @HasPermission(permission = Permission.RELEASE, action = READ) + public ReleaseEntity getReleaseById(@NonNull Integer id) throws NotFoundException { + ReleaseEntity entity = releaseRepository.find(id); + this.requireNotNull(entity); + return entity; + } + + private void requireNotNull(ReleaseEntity entity) throws NotFoundException { + if (entity == null) { + throw new NotFoundException("Release not found."); + } } public List getReleasesForResourceGroup(ResourceGroupEntity resourceGroup) { return releaseRepository.getReleasesForResourceGroup(resourceGroup); } + /** + * @return the number of existing releases + */ + @HasPermission(permission = Permission.RELEASE, action = READ) + public int countReleases() { + return persistenceService.count(); + } + + /** + * Returns a list of releases for management operations. This means, we + * don't care about relations to resources or deployments but only want to + * create, edit or delete plain release-instances + */ + @HasPermission(permission = Permission.RELEASE, action = READ) + public List loadReleasesForMgmt(Integer startIndex, Integer length, boolean sortDesc) { + return persistenceService.loadReleaseEntities(startIndex, length, sortDesc); + } + + /** + * Load all releases + */ + @HasPermission(permission = Permission.RELEASE, action = READ) + public List loadAllReleases(boolean sortDesc) { + + return persistenceService.loadAllReleaseEntities(sortDesc); + } + + @HasPermission(permission = Permission.RELEASE, action = READ) + public ReleaseEntity getDefaultRelease() { + return persistenceService.getDefaultRelease(); + } + + + @HasPermission(permission = Permission.RELEASE, action = READ) + public List getResourcesForRelease(Integer releaseId) { + return persistenceService.getResourcesForRelease(releaseId); + } + + /** + * Persists the given new release. + */ + @HasPermission(permission = Permission.RELEASE, action = CREATE) + public boolean create(ReleaseEntity release) { + return persistenceService.saveReleaseEntity(release); + } + + /** + * Persists the given release - the already existing instance will be updated. + */ + @HasPermission(permission = Permission.RELEASE, action = UPDATE) + public boolean update(ReleaseEntity release) throws ConcurrentModificationException { + try { + return persistenceService.saveReleaseEntity(release); + } catch (EJBTransactionRolledbackException e) { + throw new ConcurrentModificationException("Concurrent update prevented! Please reload before updating release: " + release); + } + } + @HasPermission(permission = Permission.RELEASE, action = DELETE) public void delete(ReleaseEntity release) { releaseRepository.removeRelease(release); } + + @HasPermission(permission = Permission.RELEASE, action = READ) + public SortedMap> loadResourcesAndDeploymentsForRelease(Integer releaseId) { + SortedMap> resourcesForCurrentRelease; + List result = this.getResourcesForRelease(releaseId); + resourcesForCurrentRelease = new TreeMap(); + for (ResourceEntity r : result) { + ResourceTypeEntityDto typeDto = new ResourceTypeEntityDto(r.getResourceType().getId(), r.getResourceType().getName()); + ResourceEntityDto entityDto = new ResourceEntityDto(r.getId(), r.getName(), typeDto); + if (!resourcesForCurrentRelease.containsKey(typeDto.getName())) { + resourcesForCurrentRelease.put(typeDto.getName(), new TreeSet<>()); + } + resourcesForCurrentRelease.get(typeDto.getName()).add(entityDto); + } + return resourcesForCurrentRelease; + } } diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceEntityDto.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceEntityDto.java new file mode 100644 index 000000000..94c1883a2 --- /dev/null +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceEntityDto.java @@ -0,0 +1,30 @@ +package ch.puzzle.itc.mobiliar.business.releasing.boundary; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ResourceEntityDto implements Comparable { + + private Integer id; + + private String name; + + private ResourceTypeEntityDto resourceType; + + @Override + public int compareTo(ResourceEntityDto o) { + if (getName() == null) { + return -1; + } + if (o == null) { + return 1; + } + int c = getName().compareToIgnoreCase(o.getName()); + if (c == 0) { + return getId() != null ? getId().compareTo(o.getId()) : o.getId() == null ? 0 : -1; + } + return c; + } +} diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceTypeEntityDto.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceTypeEntityDto.java new file mode 100644 index 000000000..10a2074ce --- /dev/null +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/boundary/ResourceTypeEntityDto.java @@ -0,0 +1,24 @@ +package ch.puzzle.itc.mobiliar.business.releasing.boundary; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ResourceTypeEntityDto implements Comparable { + + private Integer id; + + private String name; + + @Override + public int compareTo(ResourceTypeEntityDto o) { + if (name == null) { + return -1; + } + if (o == null) { + return 1; + } + return name.compareToIgnoreCase(o.getName()); + } +} diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtPersistenceService.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtPersistenceService.java index 7cdc01cbd..e7cac26e8 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtPersistenceService.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtPersistenceService.java @@ -29,6 +29,7 @@ import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.TransactionRequiredException; +import javax.persistence.TypedQuery; import javax.persistence.criteria.*; import java.util.List; import java.util.logging.Logger; @@ -57,11 +58,21 @@ private Order getDefaultOrder(Root root, boolean desc) { * @return a sublist of all releaseEntities ordered by installationInProductionAt starting from the given * startIndex and with the given length */ - public List loadReleaseEntities(int startIndex, int length, boolean sortDesc) { + public List loadReleaseEntities(Integer startIndex, Integer length, boolean sortDesc) { CriteriaQuery query = entityManager.getCriteriaBuilder().createQuery(ReleaseEntity.class); Root root = query.from(ReleaseEntity.class); query.orderBy(getDefaultOrder(root, sortDesc)); - return entityManager.createQuery(query).setFirstResult(startIndex).setMaxResults(length).getResultList(); + + TypedQuery typedQuery = entityManager.createQuery(query); + if (startIndex != null) { + typedQuery.setFirstResult(startIndex); + } + + if (length != null) { + typedQuery.setMaxResults(length); + } + + return typedQuery.getResultList(); } /** @@ -77,7 +88,7 @@ public List loadAllReleaseEntities(boolean sortDesc) { /** * @return the default release which is the release with the lowest date */ - public ReleaseEntity getDefaultRelease(){ + public ReleaseEntity getDefaultRelease() { CriteriaQuery query = entityManager.getCriteriaBuilder().createQuery(ReleaseEntity.class); Root root = query.from(ReleaseEntity.class); query.orderBy(entityManager.getCriteriaBuilder().asc(root.get("installationInProductionAt"))); diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtService.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtService.java index c3cd19868..ae6078247 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtService.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/control/ReleaseMgmtService.java @@ -65,16 +65,6 @@ public ReleaseEntity findByName(String name) { return persistenceService.findByName(name); } - /** - * Returns a list of releases for management operations. This means, we - * don't care about relations to resources or deployments but only want to - * create, edit or delete plain release-instances - */ - @HasPermission(permission = Permission.RELEASE, action = READ) - public List loadReleasesForMgmt(int startIndex, int length, boolean sortDesc) { - return persistenceService.loadReleaseEntities(startIndex, length, sortDesc); - } - /** * @return a list with releaseEntities, excluding the one we want to copy from */ @@ -89,38 +79,10 @@ public Map loadReleasesForCreatingNewRelease(Set getResourcesForRelease(Integer releaseId) { - return persistenceService.getResourcesForRelease(releaseId); - } - public Map getDeployableReleasesForResourceGroupWithLatestDeploymentDate(ResourceGroupEntity resourceGroup, ContextEntity context) { Map result = new LinkedHashMap<>(); diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/entity/ReleaseEntity.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/entity/ReleaseEntity.java index 6fd8976a9..2ddee98fe 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/entity/ReleaseEntity.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/releasing/entity/ReleaseEntity.java @@ -150,7 +150,6 @@ public class ReleaseEntity implements Serializable, Comparable { @Setter @Version @XmlTransient - @JsonIgnore private long v; public ReleaseEntity() { diff --git a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceLocator.java b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceLocator.java index 1ba18746f..ebfbb1154 100644 --- a/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceLocator.java +++ b/AMW_business/src/main/java/ch/puzzle/itc/mobiliar/business/resourcegroup/boundary/ResourceLocator.java @@ -30,6 +30,7 @@ import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceTypeEntity; import ch.puzzle.itc.mobiliar.business.resourcerelation.entity.ConsumedResourceRelationEntity; import ch.puzzle.itc.mobiliar.business.utils.ValidationHelper; +import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; import ch.puzzle.itc.mobiliar.common.exception.ValidationException; import ch.puzzle.itc.mobiliar.common.util.ConfigKey; import ch.puzzle.itc.mobiliar.common.util.ConfigurationService; @@ -116,7 +117,7 @@ public ReleaseEntity getExactOrClosestPastReleaseByGroupNameAndRelease(String na * @param releaseId release id * @return ResourceEntity */ - public ResourceEntity getExactOrClosestPastReleaseByGroupIdAndReleaseId(@NotNull Integer groupId, @NotNull Integer releaseId) { + public ResourceEntity getExactOrClosestPastReleaseByGroupIdAndReleaseId(@NotNull Integer groupId, @NotNull Integer releaseId) throws NotFoundException { ReleaseEntity release = releaseLocator.getReleaseById(releaseId); ResourceGroupEntity resGroup = resourceGroupRepository.getResourceGroupById(groupId); @@ -135,7 +136,7 @@ public ResourceEntity getExactOrClosestPastReleaseByGroupIdAndReleaseId(@NotNull * @param releaseId release id * @return */ - public ResourceEntity getResourceByGroupIdAndRelease(@NotNull Integer groupId, @NotNull Integer releaseId) { + public ResourceEntity getResourceByGroupIdAndRelease(@NotNull Integer groupId, @NotNull Integer releaseId) throws NotFoundException { ReleaseEntity release = releaseLocator.getReleaseById(releaseId); try { diff --git a/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/exception/ConcurrentModificationException.java b/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/exception/ConcurrentModificationException.java new file mode 100644 index 000000000..c77445af4 --- /dev/null +++ b/AMW_commons/src/main/java/ch/puzzle/itc/mobiliar/common/exception/ConcurrentModificationException.java @@ -0,0 +1,14 @@ +package ch.puzzle.itc.mobiliar.common.exception; + +public class ConcurrentModificationException extends AMWException{ + + private static final long serialVersionUID = 1L; + + public ConcurrentModificationException(String message, Throwable cause) { + super(message, cause); + } + + public ConcurrentModificationException(String message) { + super(message); + } +} diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java index 37ee29ac8..5cd5b78d1 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/RESTApplication.java @@ -89,6 +89,7 @@ private void addRestResourceClasses(Set> resources) { resources.add(NotFoundExceptionMapper.class); resources.add(UncaughtExceptionMapper.class); resources.add(ConstraintViolationExceptionMapper.class); + resources.add(ConcurrentModificationExceptionMapper.class); // health resources.add(HealthCheck.class); diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/exceptions/ConcurrentModificationExceptionMapper.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/exceptions/ConcurrentModificationExceptionMapper.java new file mode 100644 index 000000000..02cb15b19 --- /dev/null +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/exceptions/ConcurrentModificationExceptionMapper.java @@ -0,0 +1,36 @@ +/* + * AMW - Automated Middleware allows you to manage the configurations of + * your Java EE applications on an unlimited number of different environments + * with various versions, including the automated deployment of those apps. + * Copyright (C) 2013-2016 by Puzzle ITC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package ch.mobi.itc.mobiliar.rest.exceptions; + +import ch.puzzle.itc.mobiliar.common.exception.ConcurrentModificationException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + + +@Provider +public class ConcurrentModificationExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ConcurrentModificationException exception) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(new ExceptionDto(exception)).build(); + } +} diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/releases/ReleasesRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/releases/ReleasesRest.java index 8bd974139..296d4972b 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/releases/ReleasesRest.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/releases/ReleasesRest.java @@ -20,20 +20,24 @@ package ch.mobi.itc.mobiliar.rest.releases; -import java.util.List; +import ch.mobi.itc.mobiliar.rest.exceptions.ExceptionDto; +import ch.puzzle.itc.mobiliar.business.releasing.boundary.ReleaseLocator; +import ch.puzzle.itc.mobiliar.business.releasing.control.ReleaseMgmtService; +import ch.puzzle.itc.mobiliar.business.releasing.entity.ReleaseEntity; +import ch.puzzle.itc.mobiliar.common.exception.ConcurrentModificationException; +import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; +import javax.ws.rs.*; +import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; +import java.util.List; -import ch.puzzle.itc.mobiliar.business.releasing.control.ReleaseMgmtService; -import ch.puzzle.itc.mobiliar.business.releasing.entity.ReleaseEntity; -import ch.puzzle.itc.mobiliar.business.releasing.boundary.ReleaseLocator; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; +import static javax.ws.rs.core.Response.Status.*; @RequestScoped @Path("/releases") @@ -47,19 +51,86 @@ public class ReleasesRest { @GET @ApiOperation(value = "Get releases", notes = "Returns all releases") - public List getReleases() { - return releaseMgmtService.loadAllReleases(true); + public List getReleases(@QueryParam("start") Integer start, @QueryParam("limit") Integer limit) { + if (start == null && limit == null) { + return releaseLocator.loadAllReleases(true); + } else { + return releaseLocator.loadReleasesForMgmt(start, limit, true); + } } @GET @Path("/{id}") @ApiOperation(value = "Get a release", notes = "Returns the specifed release") - public Response getRelease(@PathParam("id") int id) { + public Response getRelease(@PathParam("id") int id) throws NotFoundException { + ReleaseEntity release = releaseLocator.getReleaseById(id); + return Response.ok(release).build(); + } + + + @GET() + @Path("/count") + @ApiOperation(value = "Get the number of releases", notes = "Returns the total amount of release entities") + public int getCount() { + return releaseLocator.countReleases(); + } + + + @GET() + @Path("/default") + @ApiOperation(value = "Get default release", notes = "Returns the default release entity") + public ReleaseEntity getDefaultRelease() { + return releaseLocator.getDefaultRelease(); + } + + @POST + @ApiOperation(value = "Add a release") + public Response addRelease(@ApiParam() ReleaseEntity request) { + if (request.getId() != null) { + return Response.status(BAD_REQUEST).entity(new ExceptionDto("Id must be null")).build(); + } + if (releaseLocator.create(request)) { + return Response.status(CREATED).build(); + } else { + return Response.status(BAD_REQUEST).build(); + } + } + + @PUT + @Path("/{id : \\d+}") + // support digit only + @Produces("application/json") + @ApiOperation(value = "Update a release") + public Response updateRelease(@ApiParam("Release ID") @PathParam("id") Integer id, ReleaseEntity request) throws NotFoundException, ConcurrentModificationException { + releaseLocator.getReleaseById(id); + if (releaseLocator.update(request)) { + return Response.status(OK).build(); + } else { + return Response.status(BAD_REQUEST).build(); + } + } + + @DELETE + @Path("/{id : \\d+}") + // support digit only + @ApiOperation(value = "Remove a release") + public Response deleteRelease(@ApiParam("Release ID") @PathParam("id") Integer id) throws NotFoundException { ReleaseEntity release = releaseLocator.getReleaseById(id); - if(release == null) { - return Response.status(Response.Status.NOT_FOUND).build(); + if (!releaseLocator.loadResourcesAndDeploymentsForRelease(id).keySet().isEmpty()) { + return Response.status(CONFLICT).entity(new ExceptionDto("Constraint violation. Cascade-delete is not supported. ")).build(); } + releaseLocator.delete(release); - return Response.ok(release).build(); + return Response.status(NO_CONTENT).build(); + } + + @GET + @Path("/{id : \\d+}/resources") + @ApiOperation(value = "Get resources of a release", notes = "Returns all resources for a release by id") + public Response getResourcesForRelease(@ApiParam("Release ID") @PathParam("id") Integer id) { + return Response.status(OK) + .entity( + new GenericEntity<>(releaseLocator.loadResourcesAndDeploymentsForRelease(id)) { + }).build(); } } diff --git a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java index a5c4d6356..4ba7c90e2 100644 --- a/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java +++ b/AMW_rest/src/main/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRest.java @@ -42,6 +42,7 @@ import ch.puzzle.itc.mobiliar.business.security.boundary.PermissionBoundary; import ch.puzzle.itc.mobiliar.common.exception.AMWException; import ch.puzzle.itc.mobiliar.common.exception.ElementAlreadyExistsException; +import ch.puzzle.itc.mobiliar.common.exception.NotFoundException; import ch.puzzle.itc.mobiliar.common.exception.ResourceNotFoundException; import ch.puzzle.itc.mobiliar.common.exception.ValidationException; import io.swagger.annotations.Api; @@ -286,7 +287,7 @@ public List getAllResourceGroups(@QueryParam("includeAppServer @GET @ApiOperation(value = "Get resource in specific release - used by Angular") public ResourceDTO getResourceRelationListForRelease(@PathParam("resourceGroupId") Integer resourceGroupId, - @PathParam("releaseId") Integer releaseId) { + @PathParam("releaseId") Integer releaseId) throws NotFoundException { ResourceEntity resource = resourceDependencyResolverService.getResourceEntityForRelease(resourceGroupId, releaseId); if (resource == null) { @@ -303,7 +304,7 @@ public ResourceDTO getResourceRelationListForRelease(@PathParam("resourceGroupId @DELETE @ApiOperation(value = "Delete a specific resource release") public Response deleteResourceRelease(@PathParam("resourceGroupId") Integer resourceGroupId, - @PathParam("releaseId") Integer releaseId) throws ResourceNotFoundException, ElementAlreadyExistsException, ForeignableOwnerViolationException { + @PathParam("releaseId") Integer releaseId) throws NotFoundException, ElementAlreadyExistsException, ForeignableOwnerViolationException { ResourceEntity resource = resourceLocator.getResourceByGroupIdAndRelease(resourceGroupId, releaseId); if (resource == null) { return Response.status(NOT_FOUND).entity(new ExceptionDto("Resource not found")).build(); @@ -317,7 +318,7 @@ public Response deleteResourceRelease(@PathParam("resourceGroupId") Integer reso @ApiOperation(value = "Get application with version for a specific resourceGroup, release and context(s) - used by Angular") public Response getApplicationsWithVersionForRelease(@PathParam("resourceGroupId") Integer resourceGroupId, @PathParam("releaseId") Integer releaseId, - @QueryParam("context") List contextIds) { + @QueryParam("context") List contextIds) throws NotFoundException { ResourceEntity appServer = resourceLocator.getExactOrClosestPastReleaseByGroupIdAndReleaseId(resourceGroupId, releaseId); if (appServer == null) { diff --git a/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java b/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java index 852aa1e80..4f643c2cd 100644 --- a/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java +++ b/AMW_rest/src/test/java/ch/mobi/itc/mobiliar/rest/resources/ResourceGroupsRestTest.java @@ -334,7 +334,7 @@ public void shouldInvokeResourcesWithRightArgumentsOnGetClosestPastRelease() thr } @Test - public void shouldInvokeBoundaryWithRightArgumentsOnGetApplicationsWithVersionForRelease() { + public void shouldInvokeBoundaryWithRightArgumentsOnGetApplicationsWithVersionForRelease() throws NotFoundException { // given Integer resourceGroupId = 8; Integer releaseId = 9; @@ -412,7 +412,7 @@ public void shouldFilterOutAppServerContainer() { } @Test - public void shouldDeleteResource() throws ResourceNotFoundException, ElementAlreadyExistsException, ForeignableOwnerViolationException { + public void shouldDeleteResource() throws NotFoundException, ElementAlreadyExistsException, ForeignableOwnerViolationException { // given Integer resourceGroupId = 8; Integer resourceId = 9; @@ -429,7 +429,7 @@ public void shouldDeleteResource() throws ResourceNotFoundException, ElementAlre } @Test - public void shouldReturnNotFoundWhenDeleteResource() throws ResourceNotFoundException, ElementAlreadyExistsException, ForeignableOwnerViolationException { + public void shouldReturnNotFoundWhenDeleteResource() throws NotFoundException, ElementAlreadyExistsException, ForeignableOwnerViolationException { // given Integer resourceGroupId = 8; Integer releaseId = 10; diff --git a/AMW_web/src/main/java/ch/puzzle/itc/mobiliar/presentation/release/ReleaseMgmtScreenService.java b/AMW_web/src/main/java/ch/puzzle/itc/mobiliar/presentation/release/ReleaseMgmtScreenService.java deleted file mode 100644 index cb63a2609..000000000 --- a/AMW_web/src/main/java/ch/puzzle/itc/mobiliar/presentation/release/ReleaseMgmtScreenService.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * AMW - Automated Middleware allows you to manage the configurations of - * your Java EE applications on an unlimited number of different environments - * with various versions, including the automated deployment of those apps. - * Copyright (C) 2013-2016 by Puzzle ITC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package ch.puzzle.itc.mobiliar.presentation.release; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.SortedMap; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import javax.enterprise.context.SessionScoped; -import javax.inject.Inject; -import javax.inject.Named; - -import ch.puzzle.itc.mobiliar.business.deploy.boundary.DeploymentBoundary; -import ch.puzzle.itc.mobiliar.business.releasing.boundary.ReleaseLocator; -import ch.puzzle.itc.mobiliar.business.releasing.control.ReleaseMgmtService; -import ch.puzzle.itc.mobiliar.business.releasing.entity.ReleaseEntity; -import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceEntity; -import ch.puzzle.itc.mobiliar.business.resourcegroup.entity.ResourceTypeEntity; -import ch.puzzle.itc.mobiliar.presentation.components.impl.PaginationComp; -import ch.puzzle.itc.mobiliar.presentation.util.GlobalMessageAppender; - -/** - * This screen service holds the information required for the whole view-session. - * - */ -@Named -@SessionScoped -public class ReleaseMgmtScreenService extends PaginationComp implements Serializable { - - private static final long serialVersionUID = 4495708946323626547L; - - @Inject - ReleaseMgmtService releaseMgmtService; - - @Inject - ReleaseLocator releaseLocator; - - @Inject - DeploymentBoundary deploymentBoundary; - - private List releases = new ArrayList(); - - private Integer totalCount; - - private ReleaseEntity defaultRelease; - - /** - * Get the releases available - this method loads and caches the results for fast re-use - * @return - */ - public List getReleases() { - if (releases == null || releases.isEmpty()) { - releases = releaseMgmtService.loadReleasesForMgmt(getStartIndex(), getPageSize(), true); - } - return releases; - } - - public ReleaseEntity getDefaultRelease(){ - if(defaultRelease == null){ - defaultRelease = releaseMgmtService.getDefaultRelease(); - } - return defaultRelease; - } - - /** - * Helper method to do lazy fetching of the release-count - * - * @return - */ - private int countReleases() { - if (totalCount == null) { - totalCount = releaseMgmtService.countReleases(); - } - return totalCount.intValue(); - } - - /** - * Clear the cached data to cause a reload during next rendering process - */ - private void reload(){ - releases.clear(); - totalCount = null; - currentRelease = null; - defaultRelease = null; - } - - - /*********** EDIT/REMOVE RELEASE *****************/ - - public boolean isDefaultRelease(ReleaseEntity release){ - return getDefaultRelease().equals(release); - } - - - private ReleaseEntity currentRelease; - private SortedMap> resourcesForCurrentRelease; - - public void remove(){ - releaseLocator.delete(currentRelease); - GlobalMessageAppender.addSuccessMessage("Release " + currentRelease.getName() + " successfully removed."); - reload(); - } - - /** - * Initialize the creation process of a release by calling before the "add" dialog is rendered. - */ - public void createRelease(){ - currentRelease = new ReleaseEntity(); - } - - public ReleaseEntity getCurrentRelease() { - return currentRelease; - } - - public Integer getReleaseId() { - return currentRelease!=null ? currentRelease.getId() : null; - } - - public void setReleaseId(Integer release) { - for (ReleaseEntity r : getReleases()) { - if(r.getId().equals(release)){ - currentRelease = r; - break; - } - } - } - - public void loadResourcesAndDeploymentsForRelease(Integer releaseId) { - List result = releaseMgmtService.getResourcesForRelease(releaseId); - resourcesForCurrentRelease = new TreeMap(); - for (ResourceEntity r : result) { - if (!resourcesForCurrentRelease.containsKey(r.getResourceType())) { - resourcesForCurrentRelease.put(r.getResourceType(), new TreeSet()); - } - resourcesForCurrentRelease.get(r.getResourceType()).add(r); - } - } - - public boolean hasResourcesForCurrentRelease() { - return resourcesForCurrentRelease != null - && !resourcesForCurrentRelease.isEmpty(); - } - - public List getResTypesForCurrentRelease() { - if (hasResourcesForCurrentRelease()) { - return new ArrayList<>(resourcesForCurrentRelease.keySet()); - } - return null; - } - - public List getResForCurrentReleaseByType(Integer typeId) { - if (hasResourcesForCurrentRelease()) { - for (ResourceTypeEntity t : resourcesForCurrentRelease.keySet()) { - if (t.getId().equals(typeId)) { - return new ArrayList<>(resourcesForCurrentRelease.get(t)); - } - } - } - return null; - } - - /** - * Stores the release with the corresponding service method. Please call during edit- or create-form submission. - */ - public boolean save() { - if (currentRelease.getId() == null) { - releaseMgmtService.create(currentRelease); - } else { - releaseMgmtService.update(currentRelease); - } - GlobalMessageAppender.addSuccessMessage("Release " + currentRelease.getName() - + " successfully saved."); - reload(); - return true; - } - - @Override - public void reloadData() { - reload(); - } - - @Override - public int getTotalCount() { - return countReleases(); - } -} \ No newline at end of file diff --git a/AMW_web/src/main/webapp/pages/components/releasesScreenComp.xhtml b/AMW_web/src/main/webapp/pages/components/releasesScreenComp.xhtml deleted file mode 100644 index 9862f62b1..000000000 --- a/AMW_web/src/main/webapp/pages/components/releasesScreenComp.xhtml +++ /dev/null @@ -1,201 +0,0 @@ - - - - - - - - - - -

- -

-
- - - - -
-
-
- -
- - - - -
-
-
- -
- - - - -
-
-
- -
- - - - -
-
-
- - - -
- - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - - - -

- - -

-
- - - x - - - - - - - - - - - - - - - - - - -
-
- - - - - -

- -

-
- - - x - - - - -
- -


-
    - -
  • -
      - -
    • -
      -
  • -
    -
-
- - -
- -
-
- - \ No newline at end of file diff --git a/AMW_web/src/main/webapp/pages/settings.xhtml b/AMW_web/src/main/webapp/pages/settings.xhtml index 0d64eb4e3..e93b4dde2 100644 --- a/AMW_web/src/main/webapp/pages/settings.xhtml +++ b/AMW_web/src/main/webapp/pages/settings.xhtml @@ -74,9 +74,10 @@
  • - - - + + Releases +
  • - - - - - - -