diff --git a/smp-angular/e2e/app.e2e-spec.ts b/smp-angular/e2e/app.e2e-spec.ts index c19f4bb57d493a22833f1649afeda698efe5d8fe..08f1001f7aa25d6c45be027ef13d7e4b118c8c80 100644 --- a/smp-angular/e2e/app.e2e-spec.ts +++ b/smp-angular/e2e/app.e2e-spec.ts @@ -1,6 +1,6 @@ import { SmpAngular2WebPage } from './app.po'; -describe('domibus-MSH-web App', function() { +describe('smp-MSH-web App', function() { let page: SmpAngular2WebPage; beforeEach(() => { diff --git a/smp-angular/src/app/app.module.ts b/smp-angular/src/app/app.module.ts index c020feac1a525e279c611200ccedc026267400d4..a84d410edd314a159c2993144f6a21d4649d8ad0 100644 --- a/smp-angular/src/app/app.module.ts +++ b/smp-angular/src/app/app.module.ts @@ -69,6 +69,8 @@ import {DomainDetailsDialogComponent} from "./domain/domain-details-dialog/domai import {UserDetailsDialogComponent} from "./user/user-details-dialog/user-details-dialog.component"; import {DownloadService} from "./download/download.service"; import {TrustStoreService} from "./trust-store/trust-store.service"; +import {UserService} from "./user/user.service"; +import {RoleService} from "./security/role.service"; export function extendedHttpClientFactory(xhrBackend: XHRBackend, requestOptions: RequestOptions, httpEventService: HttpEventService) { return new ExtendedHttpClient(xhrBackend, requestOptions, httpEventService); @@ -158,6 +160,8 @@ export function extendedHttpClientFactory(xhrBackend: XHRBackend, requestOptions AlertService, DownloadService, TrustStoreService, + UserService, + RoleService, { provide: Http, useFactory: extendedHttpClientFactory, diff --git a/smp-angular/src/app/common/search-table/search-table-controller.ts b/smp-angular/src/app/common/search-table/search-table-controller.ts index 4bce65d93f7828ef707393e616d5496e47455a4a..534c99864167a585f00af1c639606f12a10e53d3 100644 --- a/smp-angular/src/app/common/search-table/search-table-controller.ts +++ b/smp-angular/src/app/common/search-table/search-table-controller.ts @@ -1,5 +1,10 @@ +import {MdDialogConfig, MdDialogRef} from "@angular/material"; +import {SearchTableEntity} from "./search-table-entity.model"; + export interface SearchTableController { showDetails(row); edit(row); delete(row); + newRow(): SearchTableEntity; + newDialog(config?: MdDialogConfig): MdDialogRef<any>; } diff --git a/smp-angular/src/app/common/search-table/search-table-entity-status.model.ts b/smp-angular/src/app/common/search-table/search-table-entity-status.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..9a4a22120c5d02dbce4a07c2b291888d541b4da2 --- /dev/null +++ b/smp-angular/src/app/common/search-table/search-table-entity-status.model.ts @@ -0,0 +1,6 @@ +export enum SearchTableEntityStatus { + PERSISTED, + UPDATED, + NEW, + REMOVED +} diff --git a/smp-angular/src/app/common/search-table/search-table-entity.model.ts b/smp-angular/src/app/common/search-table/search-table-entity.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..572e888d8a51db84942da8b26403ab7031e0f69c --- /dev/null +++ b/smp-angular/src/app/common/search-table/search-table-entity.model.ts @@ -0,0 +1,6 @@ +import {SearchTableEntityStatus} from "./search-table-entity-status.model"; + +export interface SearchTableEntity { + status: SearchTableEntityStatus; + deleted?: boolean; +} diff --git a/smp-angular/src/app/common/search-table/search-table-result.model.ts b/smp-angular/src/app/common/search-table/search-table-result.model.ts index 9c6c6ad72156b1f4a1906e5adc2f0310deb99b1f..2588f29c6c91a58b33e63d1f6f58961f5d223252 100644 --- a/smp-angular/src/app/common/search-table/search-table-result.model.ts +++ b/smp-angular/src/app/common/search-table/search-table-result.model.ts @@ -1,5 +1,7 @@ +import {SearchTableEntity} from "./search-table-entity.model"; + export interface SearchTableResult { - serviceEntities: Array<any>; + serviceEntities: Array<SearchTableEntity>; pageSize: number; count: number; filter: any; diff --git a/smp-angular/src/app/common/search-table/search-table.component.css b/smp-angular/src/app/common/search-table/search-table.component.css index d1a612141160e70f00bfe5aee114e010052ee4a5..163929e63bb1ffe88f047c87a83d0045c52a8263 100644 --- a/smp-angular/src/app/common/search-table/search-table.component.css +++ b/smp-angular/src/app/common/search-table/search-table.component.css @@ -12,7 +12,16 @@ position: fixed; } - -.datatable-body{ +.datatable-body { overflow-y: scroll; } + +/deep/ .deleted span[title] { + text-decoration: line-through !important; +} + +.group-action-button { + position: absolute; + left: 8px; + bottom: 8px; +} diff --git a/smp-angular/src/app/common/search-table/search-table.component.html b/smp-angular/src/app/common/search-table/search-table.component.html index 3135c062c9ac7c07366d12d102ad89044829b9b0..a2f392c20a1273ba517af2c6ac75764215f5849d 100644 --- a/smp-angular/src/app/common/search-table/search-table.component.html +++ b/smp-angular/src/app/common/search-table/search-table.component.html @@ -3,9 +3,7 @@ <div class="selectionCriteria"> <md-card> - <md-card-content> - <div class="panel"> <form name="filterForm" #filterForm="ngForm" (ngSubmit)="search()"> <ng-container *ngTemplateOutlet="searchPanel"></ng-container> @@ -24,22 +22,22 @@ <div class="panel" style="position: absolute; top: 270px; bottom: 5px; left: 5px; right: 5px;"> <div class="group-filter-button"> - <span class="row-button"> - <app-row-limiter [pageSizes]="rowLimiter.pageSizes" - (onPageSizeChanged)="changePageSize($event.value)"></app-row-limiter> - </span> + <span class="row-button"> + <app-row-limiter [pageSizes]="rowLimiter.pageSizes" + (onPageSizeChanged)="changePageSize($event.value)"></app-row-limiter> + </span> <span class="column-filter-button"> - <app-column-picker [allColumns]="columnPicker.allColumns" [selectedColumns]="columnPicker.selectedColumns" - (onSelectedColumnsChanged)="columnPicker.changeSelectedColumns($event)"></app-column-picker> - </span> + <app-column-picker [allColumns]="columnPicker.allColumns" [selectedColumns]="columnPicker.selectedColumns" + (onSelectedColumnsChanged)="columnPicker.changeSelectedColumns($event)"></app-column-picker> + </span> </div> <!-- temporal solution <div - absolut - wrapping> for stretch table height to fit screen size: scrollbarV does not work - virtual scrolling has row bugs.--> - <div class="panel" style="position: absolute; overflow-y: scroll;top: 100px; bottom: 40px; left: 0px; right: 0px;"> + <div class="panel"> <ngx-datatable - id="serviceGroupTable" + id="searchTable" class="material striped" - style="" + [rowClass]="getRowClass" [rows]="rows" [columns]="columnPicker.selectedColumns" [columnMode]="'force'" @@ -50,62 +48,55 @@ [externalPaging]="true" [externalSorting]="true" [loadingIndicator]="loading" - [count]="count" + [count]="rows.length" [offset]="offset" [limit]="rowLimiter.pageSize" - [sorts]="[{prop: 'received', dir: 'desc'}]" - (page)='onPage($event)' + (page)="onPage($event)" (sort)="onSort($event)" [selected]="selected" [selectionType]="'multi'" (activate)="onActivate($event)" (select)="onSelect($event)"> </ngx-datatable> - </div> - - <ng-template #rowActions let-row="row" let-value="value" ngx-datatable-cell-template> - - <button md-icon-button color="primary" - (click)="editRowButtonAction(row)" id="editButtonRow{{row.$$index}}_id" tooltip="Edit"> - <md-icon>edit</md-icon> - </button> - - - <button md-icon-button color="primary" (click)="deleteRowButtonAction(row)" - id="deleteButtonRow{{row.$$index}}_id" tooltip="Delete"> - <md-icon>delete</md-icon> - </button> - </ng-template> - - - <div class="group-action-button" style="position: absolute;left: 8px; bottom: 8px;"> - <button md-raised-button color="primary" (click)="newButtonAction()" - id="add_id"> - <md-icon>add</md-icon> - <span>New</span> - </button> - - <button md-raised-button color="primary" [disabled]="!isRowSelected()" (click)="editButtonAction()" - id="edit_id"> - <md-icon>edit</md-icon> - <span>Edit</span> - </button> - - <button md-raised-button color="primary" [disabled]="!isRowSelected()" (click)="deleteButtonAction()" - id="resendbutton_id"> - <md-icon>delete</md-icon> - <span>Delete</span> - </button> - - <ng-container *ngTemplateOutlet="additionalToolButtons"></ng-container> + <div class="group-action-button"> + <button id="cancelButton" md-raised-button (click)="onCancelButtonClicked()" color="primary" [disabled]="!submitButtonsEnabled"> + <md-icon>cancel</md-icon> + <span>Cancel</span> + </button> + <button id="saveButton" md-raised-button (click)="onSaveButtonClicked(false)" color="primary" [disabled]="!submitButtonsEnabled"> + <md-icon>save</md-icon> + <span>Save</span> + </button> + <button id="newButton" md-raised-button (click)="onNewButtonClicked()" [disabled]="loading" color="primary"> + <md-icon>add</md-icon> + <span>New</span> + </button> + <button id="editButton" md-raised-button (click)="onEditButtonClicked()" [disabled]="!editButtonEnabled || loading" color="primary"> + <md-icon>edit</md-icon> + <span>Edit</span> + </button> + <button id="deleteButton" md-raised-button (click)="onDeleteButtonClicked()" [disabled]="!deleteButtonEnabled || loading" color="primary"> + <md-icon>delete</md-icon> + <span>Delete</span> + </button> + + <ng-container *ngTemplateOutlet="additionalToolButtons"></ng-container> + + </div> + + <ng-template #rowActions let-row="row" ngx-datatable-cell-template> + <div> + <button md-icon-button color="primary" [disabled]="row.deleted || loading" + (click)="onEditRowActionClicked(row.$$index)" tooltip="Edit"> + <md-icon>edit</md-icon> + </button> + <button md-icon-button color="primary" [disabled]="row.deleted || loading" + (click)="onDeleteRowActionClicked(row)" tooltip="Delete"> + <md-icon>delete</md-icon> + </button> + </div> + </ng-template> </div> </div> - - </div> - -<!-- - - ---> diff --git a/smp-angular/src/app/common/search-table/search-table.component.ts b/smp-angular/src/app/common/search-table/search-table.component.ts index 294f8b26b9533d949c002e3717d6b3d2d8c7a7dd..3fc0522697aebd76a92d9cd4c0f34e7224f0ae83 100644 --- a/smp-angular/src/app/common/search-table/search-table.component.ts +++ b/smp-angular/src/app/common/search-table/search-table.component.ts @@ -1,5 +1,5 @@ -import {Component, EventEmitter, Input, OnInit, TemplateRef, ViewChild} from "@angular/core"; -import {Http, URLSearchParams, Response} from "@angular/http"; +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit, TemplateRef, ViewChild} from "@angular/core"; +import {Http, Response, URLSearchParams} from "@angular/http"; import {SearchTableResult} from "./search-table-result.model"; import {Observable} from "rxjs"; import {AlertService} from "../../alert/alert.service"; @@ -8,13 +8,18 @@ import {ColumnPicker} from "../column-picker/column-picker.model"; import {RowLimiter} from "../row-limiter/row-limiter.model"; import {AlertComponent} from "../../alert/alert.component"; import {SearchTableController} from "./search-table-controller"; +import {finalize, map} from "rxjs/operators"; +import {SearchTableEntity} from "./search-table-entity.model"; +import {SearchTableEntityStatus} from "./search-table-entity-status.model"; +import {CancelDialogComponent} from "../cancel-dialog/cancel-dialog.component"; +import {SaveDialogComponent} from "../save-dialog/save-dialog.component"; +import {DownloadService} from "../../download/download.service"; @Component({ selector: 'smp-search-table', templateUrl: './search-table.component.html', styleUrls: ['./search-table.component.css'] }) - export class SearchTableComponent implements OnInit { @ViewChild('rowActions') rowActions: TemplateRef<any>; @@ -28,26 +33,26 @@ export class SearchTableComponent implements OnInit { @Input() searchTableController: SearchTableController; @Input() filter: any = {}; - columnActions:any; + loading = false; + + columnActions: any; rowLimiter: RowLimiter = new RowLimiter(); - selected = []; + rowNumber: number; + + rows: Array<SearchTableEntity> = []; + selected: Array<SearchTableEntity> = []; - loading: boolean = false; - rows = []; count: number = 0; offset: number = 0; - //default value orderBy: string = null; - //default value - asc: boolean = false; + asc = false; - msgStatus: Array<String>; - - messageResent = new EventEmitter(false); - - constructor(protected http: Http, protected alertService: AlertService, public dialog: MdDialog) { + constructor(protected http: Http, + protected alertService: AlertService, + private downloadService: DownloadService, + public dialog: MdDialog) { } ngOnInit() { @@ -57,24 +62,30 @@ export class SearchTableComponent implements OnInit { width: 80, sortable: false }; - /** - * Add actions to last column - */ + + // Add actions to last column if (this.columnPicker) { this.columnPicker.allColumns.push(this.columnActions); - this.columnPicker.selectedColumns.push(this.columnActions); } this.page(this.offset, this.rowLimiter.pageSize, this.orderBy, this.asc); } - getTableDataEntries(offset: number, pageSize: number, orderBy: string, asc: boolean): Observable< SearchTableResult > { + getRowClass(row): string { + return row.deleted ? 'deleted' : ''; + } + + getTableDataEntries$(offset: number, pageSize: number, orderBy: string, asc: boolean): Observable<SearchTableResult> { let searchParams: URLSearchParams = new URLSearchParams(); searchParams.set('page', offset.toString()); searchParams.set('pageSize', pageSize.toString()); searchParams.set('orderBy', orderBy); //filters + if (this.filter.userName) { + searchParams.set('userName', this.filter.userName); + } + if (this.filter.participantId) { searchParams.set('participantId', this.filter.participantId); } @@ -83,7 +94,6 @@ export class SearchTableComponent implements OnInit { searchParams.set('participantSchema', this.filter.participantSchema); } - if(this.filter.domain) { searchParams.set('domain', this.filter.domain ) } @@ -92,114 +102,212 @@ export class SearchTableComponent implements OnInit { searchParams.set('asc', asc.toString()); } + // TODO move to the HTTP service + this.loading = true; return this.http.get(this.url, { search: searchParams - }).map((response: Response) => - response.json() + }).pipe( + map((response: Response) => response.json()), + finalize(() => { + this.loading = false; + }) ); } - page(offset, pageSize, orderBy, asc) { - this.loading = true; - - this.getTableDataEntries(offset, pageSize, orderBy, asc).subscribe((result: SearchTableResult ) => { - console.log("service group response:" + result); + page(offset: number, pageSize: number, orderBy: string, asc: boolean) { + this.getTableDataEntries$(offset, pageSize, orderBy, asc).subscribe((result: SearchTableResult ) => { this.offset = offset; this.rowLimiter.pageSize = pageSize; this.orderBy = orderBy; this.asc = asc; - this.count = result.count; - this.selected = []; + this.unselectRows(); + const count = result.count; const start = offset * pageSize; - const end = start + pageSize; + const end = Math.min(start + pageSize, count); const newRows = [...result.serviceEntities]; let index = 0; for (let i = start; i < end; i++) { - newRows[i] = result.serviceEntities[index++]; + newRows[i] = {...result.serviceEntities[index++], + status: SearchTableEntityStatus.PERSISTED, + deleted: false + }; } - this.rows = newRows; - this.loading = false; - - if(this.count > AlertComponent.MAX_COUNT_CSV) { + if(count > AlertComponent.MAX_COUNT_CSV) { this.alertService.error("Maximum number of rows reached for downloading CSV"); } }, (error: any) => { - console.log("error getting the message log:" + error); - this.loading = false; - this.alertService.error("Error occured:" + error); + this.alertService.error("Error occurred:" + error); }); } onPage(event) { - console.log('Page Event', event); this.page(event.offset, event.pageSize, this.orderBy, this.asc); } onSort(event) { - console.log('Sort Event', event); - let ascending = true; - if (event.newValue === 'desc') { - ascending = false; - } + let ascending = event.newValue !== 'desc'; this.page(this.offset, this.rowLimiter.pageSize, event.column.prop, ascending); } onSelect({selected}) { - // console.log('Select Event', selected, this.selected); + this.selected = [...selected]; + if(this.editButtonEnabled) { + this.rowNumber = this.selected[0]["$$index"]; + } } onActivate(event) { - // console.log('Activate Event', event); - if ("dblclick" === event.type) { this.details(event.row); } } changePageSize(newPageLimit: number) { - console.log('New page limit:', newPageLimit); this.page(0, newPageLimit, this.orderBy, this.asc); } search() { - console.log("Searching using filter:" + this.filter); this.page(0, this.rowLimiter.pageSize, this.orderBy, this.asc); } - isRowSelected() { - if (this.selected) - return true; + details(selectedRow: any) { + this.searchTableController.showDetails(selectedRow); + } + + onEditRowActionClicked(rowNumber: number) { + this.editSearchTableEntity(rowNumber); + } - return false; + onDeleteRowActionClicked(row: SearchTableEntity) { + this.deleteSearchTableEntities([row]); } + onNewButtonClicked() { + const formRef: MdDialogRef<any> = this.searchTableController.newDialog({ + data: { edit: false } + }); + formRef.afterClosed().subscribe(result => { + if (result) { + this.rows = [...this.rows, {...formRef.componentInstance.current}]; + } else { + this.unselectRows(); + } + }); + } - details(selectedRow: any) { - this.searchTableController.showDetails(selectedRow); + onDeleteButtonClicked() { + this.deleteSearchTableEntities(this.selected); + } + + onEditButtonClicked() { + if (this.rowNumber >= 0 && this.rows[this.rowNumber] && this.rows[this.rowNumber].deleted) { + this.alertService.error('You cannot edit a deleted entry.', false); + return; + } + this.editSearchTableEntity(this.rowNumber); + } + + onSaveButtonClicked(withDownloadCSV: boolean) { + try { + // TODO: add validation support to existing controllers + // const isValid = this.userValidatorService.validateUsers(this.users); + // if (!isValid) return; + + this.dialog.open(SaveDialogComponent).afterClosed().subscribe(result => { + if (result) { + // this.unselectRows(); + const modifiedUsers = this.rows.filter(el => el.status !== SearchTableEntityStatus.PERSISTED); + // this.isBusy = true; + this.http.put(/*UserComponent.USER_USERS_URL TODO: use PUT url*/'', modifiedUsers).subscribe(res => { + // this.isBusy = false; + // this.getUsers(); + this.alertService.success('The operation \'update\' completed successfully.', false); + if (withDownloadCSV) { + this.downloadService.downloadNative(/*UserComponent.USER_CSV_URL TODO: use CSV url*/ ''); + } + }, err => { + // this.isBusy = false; + // this.getUsers(); + this.alertService.exception('The operation \'update\' not completed successfully.', err, false); + }); + } else { + if (withDownloadCSV) { + this.downloadService.downloadNative(/*UserComponent.USER_CSV_URL TODO: use CSV url*/ ''); + } + } + }); + } catch (err) { + // this.isBusy = false; + this.alertService.exception('The operation \'update\' completed with errors.', err); + } + } + + onCancelButtonClicked() { + this.dialog.open(CancelDialogComponent).afterClosed().subscribe(result => { + if (result) { + this.page(this.offset, this.rowLimiter.pageSize, this.orderBy, this.asc); + } + }); } - newButtonAction(){ + getRowsAsString(): number { + return this.rows.length; + } + get editButtonEnabled(): boolean { + return this.selected && this.selected.length == 1 && !this.selected[0].deleted; } - editButtonAction(){ - this.editRowButtonAction( this.selected[0]); + get deleteButtonEnabled(): boolean { + return this.selected && this.selected.length > 0 && !this.selected.every(el => el.deleted); } - deleteButtonAction(){ - // delete all seleted rows + get submitButtonsEnabled(): boolean { + const rowsDeleted = !!this.rows.find(row => row.deleted); + const dirty = rowsDeleted || !!this.rows.find(el => el.status !== SearchTableEntityStatus.PERSISTED); + return dirty; } - editRowButtonAction(row: any){ - this.details(row); + private editSearchTableEntity(rowNumber: number) { + const row = this.rows[rowNumber]; + const formRef: MdDialogRef<any> = this.searchTableController.newDialog({ + data: {edit: true, row} + }); + formRef.afterClosed().subscribe(result => { + if (result) { + const status = row.status === SearchTableEntityStatus.PERSISTED + ? SearchTableEntityStatus.UPDATED + : row.status; + this.rows[rowNumber] = {...formRef.componentInstance.current, status}; + this.rows = [...this.rows]; + } + }); } - deleteRowButtonAction(row: any){ + private deleteSearchTableEntities(rows: Array<SearchTableEntity>) { + // TODO: add validation support to existing controllers + // if (this.searchTableController.validateDeleteOperation(rows)) { + // this.alertService.error('You cannot delete the logged in user: ' + this.securityService.getCurrentUser().username); + // return; + // } + + for (const row of rows) { + if (row.status === SearchTableEntityStatus.NEW) { + this.rows.splice(this.rows.indexOf(row), 1); + } else { + row.status = SearchTableEntityStatus.REMOVED; + row.deleted = true; + } + } + this.unselectRows() } + private unselectRows() { + this.selected = []; + } } diff --git a/smp-angular/src/app/domain/domain-controller.ts b/smp-angular/src/app/domain/domain-controller.ts index b5b62d4a96ff99225b1cb9758fed4eb1522de3aa..3748b9156e6b27fc9459d8330b9b6d2243c9444e 100644 --- a/smp-angular/src/app/domain/domain-controller.ts +++ b/smp-angular/src/app/domain/domain-controller.ts @@ -1,6 +1,8 @@ import {SearchTableController} from "../common/search-table/search-table-controller"; -import {MdDialog, MdDialogRef} from "@angular/material"; +import {MdDialog, MdDialogConfig, MdDialogRef} from "@angular/material"; import {DomainDetailsDialogComponent} from "./domain-details-dialog/domain-details-dialog.component"; +import {DomainRo} from "./domain-ro.model"; +import {SearchTableEntityStatus} from "../common/search-table/search-table-entity-status.model"; export class DomainController implements SearchTableController { @@ -17,4 +19,19 @@ export class DomainController implements SearchTableController { public edit(row: any) { } public delete(row: any) { } + + public newDialog(config?: MdDialogConfig): MdDialogRef<DomainDetailsDialogComponent> { + return this.dialog.open(DomainDetailsDialogComponent, config); + } + + public newRow(): DomainRo { + return { + domainId: '', + bdmslClientCertHeader: '', + bdmslClientCertAlias: '', + bdmslSmpId: '', + signatureCertAlias: '', + status: SearchTableEntityStatus.NEW + } + } } diff --git a/smp-angular/src/app/domain/domain-ro.model.ts b/smp-angular/src/app/domain/domain-ro.model.ts index 7b90e0359c670d95d64139ff4b93fc60ab99a37e..b6c52bb9730122cc578a03133c1aaf6faedfe99f 100644 --- a/smp-angular/src/app/domain/domain-ro.model.ts +++ b/smp-angular/src/app/domain/domain-ro.model.ts @@ -1,4 +1,6 @@ -export interface DomainRo { +import {SearchTableEntity} from "../common/search-table/search-table-entity.model"; + +export interface DomainRo extends SearchTableEntity { domainId: string; bdmslClientCertHeader: string; bdmslClientCertAlias: string; diff --git a/smp-angular/src/app/login/login.component.html b/smp-angular/src/app/login/login.component.html index aae172e41b1d0ec6fbe4c0ec4af3ae3607254052..c01af67c6828283f52a25d6f9c806341d28d4b2b 100644 --- a/smp-angular/src/app/login/login.component.html +++ b/smp-angular/src/app/login/login.component.html @@ -5,7 +5,7 @@ <tr> <td> <md-input-container> - <input mdInput placeholder="Username" name="username" [(ngModel)]="model.username" #username="ngModel" + <input mdInput placeholder="Username" name="userName" [(ngModel)]="model.userName" #userName="ngModel" required id="username_id"> </md-input-container> </td> diff --git a/smp-angular/src/app/security/role.model.ts b/smp-angular/src/app/security/role.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..4322e22ec7d73fc875f00ee5c0338efc09271d9e --- /dev/null +++ b/smp-angular/src/app/security/role.model.ts @@ -0,0 +1,5 @@ +export enum Role { + SMP_ADMINISTRATOR, + SERVICE_GROUP_ADMINISTRATOR, + SYSTEM_ADMINISTRATOR, +} diff --git a/smp-angular/src/app/security/role.service.ts b/smp-angular/src/app/security/role.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3bb001205db362a0c2600418208254e9adc05e4 --- /dev/null +++ b/smp-angular/src/app/security/role.service.ts @@ -0,0 +1,20 @@ +import {Injectable} from "@angular/core"; +import {Role} from "./role.model"; + +@Injectable() +export class RoleService { + + public getLabel(role: Role): string { + switch (role) { + case Role.SMP_ADMINISTRATOR: + return 'SMP Administrator'; + case Role.SERVICE_GROUP_ADMINISTRATOR: + return 'ServiceGroup Administrator'; + case Role.SYSTEM_ADMINISTRATOR: + return 'System Administrator'; + default: + return ''; + } + } + +} diff --git a/smp-angular/src/app/service-group/service-group-controller.ts b/smp-angular/src/app/service-group/service-group-controller.ts index 8dc51671053b65f4dbd5c1d624d423584913e8ae..ab2408eb75bb3b11126e6793a283862f5009720d 100644 --- a/smp-angular/src/app/service-group/service-group-controller.ts +++ b/smp-angular/src/app/service-group/service-group-controller.ts @@ -1,17 +1,21 @@ import {SearchTableController} from "../common/search-table/search-table-controller"; -import {MdDialog, MdDialogRef} from "@angular/material"; +import {MdDialog, MdDialogConfig, MdDialogRef} from "@angular/material"; import {ServiceGroupDetailsDialogComponent} from "./service-group-details-dialog/service-group-details-dialog.component"; import {Http} from "@angular/http"; import {AlertService} from "../alert/alert.service"; import {ServiceGroupExtensionDialogComponent} from "./service-group-extension-dialog/service-group-extension-dialog.component"; import {ServiceGroupMetadataListDialogComponent} from "./service-group-metadata-list-dialog/service-group-metadata-list-dialog.component"; +import {UserDetailsDialogComponent} from "../user/user-details-dialog/user-details-dialog.component"; +import {SearchTableEntity} from "../common/search-table/search-table-entity.model"; +import {ServiceGroupRo} from "./service-group-ro.model"; +import {SearchTableEntityStatus} from "../common/search-table/search-table-entity-status.model"; export class ServiceGroupController implements SearchTableController { constructor(public dialog: MdDialog) { } public showDetails(row: any) { - let dialogRef: MdDialogRef<ServiceGroupDetailsDialogComponent> = this.dialog.open(ServiceGroupDetailsDialogComponent); + let dialogRef: MdDialogRef<ServiceGroupDetailsDialogComponent> = this.newDialog(); dialogRef.componentInstance.servicegroup = row; dialogRef.afterClosed().subscribe(result => { //Todo: @@ -37,5 +41,21 @@ export class ServiceGroupController implements SearchTableController { public edit(row: any) { } - public delete(row: any) { } + public delete(row: any) { } + + public newDialog(config?: MdDialogConfig): MdDialogRef<ServiceGroupDetailsDialogComponent> { + return this.dialog.open(ServiceGroupDetailsDialogComponent, config); + } + + public newRow(): ServiceGroupRo { + return { + domain: '', + serviceGroupROId: { + participantId: '', + participantSchema: '' + }, + status: SearchTableEntityStatus.NEW + }; + } + } diff --git a/smp-angular/src/app/service-group/service-group-ro.model.ts b/smp-angular/src/app/service-group/service-group-ro.model.ts index 13955cf6ab0f8f9b12f152271fcec07bcedd0e62..4c4427e2e460bf4af89428df96fb706d7be3628b 100644 --- a/smp-angular/src/app/service-group/service-group-ro.model.ts +++ b/smp-angular/src/app/service-group/service-group-ro.model.ts @@ -1,6 +1,7 @@ import { ServiceGroupROId } from './service-group-ro-id.model'; +import {SearchTableEntity} from "../common/search-table/search-table-entity.model"; -export interface ServiceGroupRo { +export interface ServiceGroupRo extends SearchTableEntity { serviceGroupROId: ServiceGroupROId; domain: string; } diff --git a/smp-angular/src/app/user/user-controller.ts b/smp-angular/src/app/user/user-controller.ts index 9ada4224bfea3801cfb7ad9bc91b58cd5f2fc979..ac89843415e037ebfff790f419d5b947b4ae7bc6 100644 --- a/smp-angular/src/app/user/user-controller.ts +++ b/smp-angular/src/app/user/user-controller.ts @@ -1,6 +1,8 @@ import {SearchTableController} from "../common/search-table/search-table-controller"; -import {MdDialog, MdDialogRef} from "@angular/material"; +import {MdDialog, MdDialogConfig, MdDialogRef} from "@angular/material"; import {UserDetailsDialogComponent} from "./user-details-dialog/user-details-dialog.component"; +import {UserRo} from "./user-ro.model"; +import {SearchTableEntityStatus} from "../common/search-table/search-table-entity-status.model"; export class UserController implements SearchTableController { @@ -8,7 +10,6 @@ export class UserController implements SearchTableController { public showDetails(row: any) { let dialogRef: MdDialogRef<UserDetailsDialogComponent> = this.dialog.open(UserDetailsDialogComponent); - dialogRef.componentInstance.user = row; dialogRef.afterClosed().subscribe(result => { //Todo: }); @@ -16,5 +17,17 @@ export class UserController implements SearchTableController { public edit(row: any) { } - public delete(row: any) { } + public delete(row: any) { } + + public newDialog(config?: MdDialogConfig): MdDialogRef<UserDetailsDialogComponent> { + return this.dialog.open(UserDetailsDialogComponent, config); + } + + public newRow(): UserRo { + return { + userName: '', + role: '', + status: SearchTableEntityStatus.NEW + } + } } diff --git a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html index c31301815e36538e24b8803da20b32b213fa0b4a..fb361aee12206c4df4b74fc565a6e647c1b0ce7f 100644 --- a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html +++ b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.html @@ -1,36 +1,64 @@ -<h2 md-dialog-title>User details</h2> +<h2 md-dialog-title>{{formTitle}}</h2> <md-dialog-content style="height:260px;width:650px"> <md-card> <md-card-content> - <md-input-container style="width:100%"> - <input mdInput placeholder="Username id" value="{{user.username}}" readonly/> - </md-input-container> + <div style="margin-top:15px;"> + <md-input-container style="width:100%"> + <input mdInput placeholder="Username" name="userName" id="userName" [value]="current.userName" (blur)="updateUserName($event)" [formControl]="userForm.controls['userName']" maxlength="255" required> + <div *ngIf="userForm.controls['userName'].hasError('required') && userForm.controls['userName'].touched" style="color:red; font-size: 70%">You should type an username</div> + </md-input-container> + </div> - <md-input-container style="width:100%"> - <input mdInput placeholder="isAdmin" value="{{user.idAdmin}}" readonly/> - </md-input-container> + <div style="margin-top:10px;"> + <md2-select mdInput placeholder="Role" [style.width]="'100%'" [formControl]="userForm.controls['role']" + [(ngModel)]="role" required> + <md2-option *ngFor="let item of existingRoles" [value]="item">{{getRoleLabel(item)}}</md2-option> + </md2-select> + <div *ngIf="userForm.controls['role'].hasError('required') && userForm.controls['role'].touched" style="color:red; font-size: 70%">You need to choose at least one role for this user</div> + </div> + <div style="margin-top:15px;"> + <md-input-container [style.width]="'100%'"> + <input mdInput placeholder="Password" name="password" type="password" [value]="current.password" (blur)="updatePassword($event)" [formControl]="userForm.controls['password']" [pattern]="passwordPattern" [required]="!editMode"> + <div *ngIf="!editMode && userForm.controls['password'].hasError('required') && userForm.controls['password'].touched" style="color:red; font-size: 70%">You should type a password</div> + <div *ngIf="userForm.controls['password'].dirty && userForm.controls['password'].hasError('pattern') && userForm.controls['password'].touched" style="color:red; font-size: 70%"> + Password should follow all of these rules:<br> + - Minimum length: 8 characters<br> + - Maximum length: 32 characters<br> + - At least one letter in lowercase<br> + - At least one letter in uppercase<br> + - At least one digit<br> + - At least one special character + </div> + </md-input-container> + </div> + <div> + <md-input-container [style.width]="'100%'"> + <input mdInput placeholder="Confirmation" name="confirmation" type="password" [value]="current.confirmation" [formControl]="userForm.controls['confirmation']" [required]="!editMode"> + <div *ngIf="!editMode && userForm.controls['confirmation'].hasError('required') && userForm.controls['confirmation'].touched" style="color:red; font-size: 70%">You should type a password</div> + <div *ngIf="userForm.errors?.confirmation && userForm.controls['confirmation'].touched" style="color:red; font-size: 70%">Passwords do not match</div> + </md-input-container> + </div> </md-card-content> </md-card> </md-dialog-content> -<md-dialog-actions> - <div class="group-action-button" > - <button id="ServiceGroupsSaveButton" md-raised-button color="primary" (click)="dialogRef.close({})" - style="margin-top:10px"> - <md-icon>save</md-icon> - <span>Save</span> - </button> - - - <button id="ServiceGroupsCloseButton" md-raised-button color="primary" (click)="dialogRef.close({})" - style="margin-top:10px"> - <md-icon>close</md-icon> - <span>Close</span> - </button> - </div> -</md-dialog-actions> +<table class="buttonsRow"> + <tr> + <td> + <button md-raised-button color="primary" [md-dialog-close]="true" (click)="submitForm()" [disabled]="!userForm.valid"> + <md-icon>check_circle</md-icon> + <span>OK</span> + </button> + <button md-raised-button color="primary" md-dialog-close> + <md-icon>cancel</md-icon> + <span>Cancel</span> + </button> + </td> + </tr> +</table> +<div style="text-align: right; font-size: 70%">* required fields</div> diff --git a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts index 6921a83d8b3ee21b1bc940cfc13dca72ee9fdf20..f88bdb22784f9af13324a0a499cd943386050f8f 100644 --- a/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts +++ b/smp-angular/src/app/user/user-details-dialog/user-details-dialog.component.ts @@ -1,5 +1,11 @@ -import {Component} from '@angular/core'; -import {MdDialogRef} from "@angular/material"; +import {Component, Inject} from '@angular/core'; +import {MD_DIALOG_DATA, MdDialogRef} from "@angular/material"; +import {AbstractControl, FormBuilder, FormControl, FormGroup, ValidationErrors, ValidatorFn, Validators} from "@angular/forms"; +import {UserService} from "../user.service"; +import {Role} from "../../security/role.model"; +import {RoleService} from "../../security/role.service"; +import {UserRo} from "../user-ro.model"; +import {SearchTableEntityStatus} from "../../common/search-table/search-table-entity-status.model"; @Component({ selector: 'user-details-dialog', @@ -7,10 +13,86 @@ import {MdDialogRef} from "@angular/material"; }) export class UserDetailsDialogComponent { - user; - dateFormat: String = 'yyyy-MM-dd HH:mm:ssZ'; + static readonly NEW_MODE = 'New User'; + static readonly EDIT_MODE = 'User Edit'; - constructor(public dialogRef: MdDialogRef<UserDetailsDialogComponent>) { + // readonly emailPattern = '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'; + readonly passwordPattern = '^(?=.*[A-Z])(?=.*[ !#$%&\'()*+,-./:;<=>?@\\[^_`{|}~\\\]"])(?=.*[0-9])(?=.*[a-z]).{8,32}$'; + + editMode: boolean; + formTitle: string; + userRoles = []; + existingRoles = []; + confirmation = ''; + current: UserRo & { confirmation?: string }; + userForm: FormGroup; + + private passwordConfirmationValidator: ValidatorFn = (control: FormGroup): ValidationErrors | null => { + const password = control.get('password'); + const confirmation = control.get('confirmation'); + return password && confirmation && password.value !== confirmation.value ? { confirmation: true } : null; + }; + + constructor(private dialogRef: MdDialogRef<UserDetailsDialogComponent>, + private userService: UserService, + private roleService: RoleService, + @Inject(MD_DIALOG_DATA) public data: any, + private fb: FormBuilder) { + this.editMode = data.edit; + this.formTitle = this.editMode ? UserDetailsDialogComponent.EDIT_MODE: UserDetailsDialogComponent.NEW_MODE; + + this.current = this.editMode + ? { + ...data.row, + confirmation: data.row.password + } + : { + userName: '', + password: '', + confirmation: '', + role: '', + status: SearchTableEntityStatus.NEW + }; + + this.userForm = fb.group({ + 'userName': new FormControl({value: this.current.userName, disabled: this.editMode}, this.editMode ? Validators.nullValidator : null), + 'role': new FormControl(this.current.role, Validators.required), + 'password': new FormControl(this.current.password, [Validators.required, Validators.pattern(this.passwordPattern)]), + 'confirmation': new FormControl(this.current.password, Validators.pattern(this.passwordPattern)) + }, { + validator: this.passwordConfirmationValidator + }); + + this.userService.getUserRoles$().subscribe(userRoles => { + this.userRoles = userRoles.json(); + this.existingRoles = this.editMode + ? this.getAllowedRoles(this.userRoles, this.current.role) + : this.userRoles; + }); + } + + submitForm() { + this.dialogRef.close(true); } + updateUserName(event) { + this.current.userName = event.target.value; + } + + updatePassword(event) { + this.current.password = event.target.value; + } + + getRoleLabel(role: Role): string { + return this.roleService.getLabel(role); + } + + // filters out roles so that the user cannot change from system administrator to the other roles or vice-versa + private getAllowedRoles(allRoles, userRole) { + if (userRole === Role.SYSTEM_ADMINISTRATOR) { + return [Role.SYSTEM_ADMINISTRATOR]; + } else { + return allRoles.filter(role => role !== Role.SYSTEM_ADMINISTRATOR); + } + } } diff --git a/smp-angular/src/app/user/user-ro.model.ts b/smp-angular/src/app/user/user-ro.model.ts index 5642811adb043cc5a40f6eaee4ed124abba0a576..fc604cea8d38cf58212a444f893f2c8b715882c1 100644 --- a/smp-angular/src/app/user/user-ro.model.ts +++ b/smp-angular/src/app/user/user-ro.model.ts @@ -1,4 +1,8 @@ -export interface UserRo { - username: string; - admin: string; +import {SearchTableEntity} from "../common/search-table/search-table-entity.model"; + +export interface UserRo extends SearchTableEntity { + userName: string; + password?: string; + role: string; + suspended?: boolean; } diff --git a/smp-angular/src/app/user/user.component.css b/smp-angular/src/app/user/user.component.css index 94bdbcf05b52992fba8266a500a289e9ed65f3b8..1e2c4567531ce54920b3da930aeb44ed2c39f44e 100644 --- a/smp-angular/src/app/user/user.component.css +++ b/smp-angular/src/app/user/user.component.css @@ -11,5 +11,3 @@ #hiddenButtonId { position: fixed; } - - diff --git a/smp-angular/src/app/user/user.component.html b/smp-angular/src/app/user/user.component.html index d14b4cd377464035304dadc7b72aea03f231f775..cf3b4eef12ef2ba50900b5278a90e59eb6adf0d3 100644 --- a/smp-angular/src/app/user/user.component.html +++ b/smp-angular/src/app/user/user.component.html @@ -1,31 +1,18 @@ - <smp-search-table page_id= 'user_id' title= 'Users' [columnPicker] = "columnPicker" - url="ui/user" + [url]="'ui/user'" [additionalToolButtons]="additionalToolButtons" [searchTableController]="userController" [searchPanel]="searchPanel" - [filter]="filter" - -> - + [filter]="filter"> - <ng-template #additionalToolButtons > - - </ng-template> + <ng-template #additionalToolButtons></ng-template> <ng-template #searchPanel> <md-input-container> - <input mdInput placeholder="Username" name="Username" [(ngModel)]="filter.messageId" - #messageId="ngModel" id="messageid_id"> + <input mdInput placeholder="Username" name="Username" [(ngModel)]="filter.userName" #messageId="ngModel"> </md-input-container> - <md-input-container> - <input mdInput placeholder="isAdmin" name="isAdmin" [(ngModel)]="filter.messageId" - #messageId="ngModel" id="participanschema_id"> - </md-input-container> - </ng-template> - </smp-search-table> diff --git a/smp-angular/src/app/user/user.component.ts b/smp-angular/src/app/user/user.component.ts index bcac77b2ba171b483a21743a333a4e46fe231a8d..ebb9a4b37ebcf0c5d6d57fc44804343a3b2cf24e 100644 --- a/smp-angular/src/app/user/user.component.ts +++ b/smp-angular/src/app/user/user.component.ts @@ -7,7 +7,6 @@ import {AlertService} from "../alert/alert.service"; import {UserController} from "./user-controller"; @Component({ - moduleId: module.id, templateUrl:'./user.component.html', styleUrls: ['./user.component.css'] }) @@ -30,23 +29,29 @@ export class UserComponent implements OnInit { this.columnPicker.allColumns = [ { name: 'Username', - prop: 'username', - width: 275 + prop: 'userName', + canAutoResize: true }, { - name: 'isAdmin', - prop: 'isadmin', - width: 40 + name: 'Role', + prop: 'role', + canAutoResize: true + }, + { + name: 'Password', + prop: 'password', + canAutoResize: true, + sortable: false, + width: 25 } ]; this.columnPicker.selectedColumns = this.columnPicker.allColumns.filter(col => { - return ["Username", "isAdmin"].indexOf(col.name) != -1 + return ['Username', 'Role'].indexOf(col.name) != -1 }); } details(row: any) { this.userController.showDetails(row); - } } diff --git a/smp-angular/src/app/user/user.service.ts b/smp-angular/src/app/user/user.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..998b81b54cccc935ca0a0c79181cf4f3609cea9e --- /dev/null +++ b/smp-angular/src/app/user/user.service.ts @@ -0,0 +1,16 @@ +import {Injectable} from "@angular/core"; +import {Observable, Subject} from "rxjs"; +import {Http} from "@angular/http"; +import {Role} from "../security/role.model"; + +@Injectable() +export class UserService { + + constructor(private http: Http) {} + + getUserRoles$() { + // return this.http.get('rest/user/userroles'); + // TODO create the endpoint + return Observable.of({json: () => [Role.SMP_ADMINISTRATOR, Role.SERVICE_GROUP_ADMINISTRATOR, Role.SYSTEM_ADMINISTRATOR]}); + } +} diff --git a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java index 0547cec7f749b7e1d4d98800332894a8c9b6f5bc..5107877606bffa8d304efaa193ee1b63f6c7265d 100644 --- a/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java +++ b/smp-server-library/src/main/java/eu/europa/ec/edelivery/smp/data/ui/UserRO.java @@ -1,15 +1,10 @@ package eu.europa.ec.edelivery.smp.data.ui; -import lombok.EqualsAndHashCode; -import lombok.ToString; - import javax.persistence.*; import java.io.Serializable; import java.util.Objects; -import static eu.europa.ec.edelivery.smp.data.model.CommonColumnsLengths.MAX_USERNAME_LENGTH; - /** * @author Joze Rihtarsic * @since 4.1 @@ -23,7 +18,7 @@ public class UserRO implements Serializable { private static final long serialVersionUID = -4971552086560325302L; @Id @Column(name = "username") - private String username; + private String userName; @Column(name = "password") private String password; @Column(name = "isadmin") @@ -33,18 +28,18 @@ public class UserRO implements Serializable { } - public UserRO(String username, String password, boolean isAdmin) { - this.username = username; + public UserRO(String userName, String password, boolean isAdmin) { + this.userName = userName; this.password = password; this.isAdmin = isAdmin; } - public String getUsername() { - return username; + public String getUserName() { + return userName; } - public void setUsername(String username) { - this.username = username; + public void setUserName(String userName) { + this.userName = userName; } public String getPassword() { @@ -68,12 +63,12 @@ public class UserRO implements Serializable { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserRO userRO = (UserRO) o; - return Objects.equals(username, userRO.username); + return Objects.equals(userName, userRO.userName); } @Override public int hashCode() { - return Objects.hash(username); + return Objects.hash(userName); } } diff --git a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ServiceUIDataIntegrationTest.java b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ServiceUIDataIntegrationTest.java index 8bd592970b0e9071860d658574bff729d8629fea..063af4d545b4a4fcaf4821d967f1d1cd225cd3f2 100644 --- a/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ServiceUIDataIntegrationTest.java +++ b/smp-server-library/src/test/java/eu/europa/ec/edelivery/smp/services/ServiceUIDataIntegrationTest.java @@ -64,7 +64,7 @@ public class ServiceUIDataIntegrationTest { for (int i = 0; i < 20; i++) { UserRO ent = new UserRO(); ent.setAdmin(false); - ent.setUsername("Username" + i); + ent.setUserName("Username" + i); ent.setPassword("Password"); serviceUIData.persistUser(ent); } @@ -128,7 +128,7 @@ public class ServiceUIDataIntegrationTest { UserRO ent = new UserRO(); ent.setAdmin(false); - ent.setUsername("Username"); + ent.setUserName("Username"); ent.setPassword("Password"); long cnt = serviceUIData.getUserList(0, 10, null, null).getCount(); diff --git a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java index 0eea0022fbe8f69ae6ae8884a72c1d7d894a0a6b..f225ed206438bf2279cb62e74cb09c2c183eb799 100644 --- a/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java +++ b/smp-webapp/src/main/java/eu/europa/ec/edelivery/smp/ui/UserResource.java @@ -33,7 +33,7 @@ public class UserResource { @PutMapping(produces = {"application/json"}) @ResponseBody @RequestMapping(method = RequestMethod.GET) - public ServiceResult<UserRO> getUserist( + public ServiceResult<UserRO> getUsers( @RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "pageSize", defaultValue = "10") int pageSize, @RequestParam(value = "orderBy", required = false) String orderBy,