diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.module.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.module.ts index e33d8e6a1b..21b572e194 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.module.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.module.ts @@ -145,9 +145,13 @@ import {UploadFileComponent} from '../sharedLearningUpload/components/uploadFile import {FilesTableComponent} from '../sharedLearningUpload/components/filesTable/filesTable.component'; import { ConfirmationModalComponent -} from '../sharedLearningUpload/components/confirmationModal/confirmationModal.component'; +} from '../Shared/components/confirmationModal/confirmationModal.component'; import {OncHistoryUploadComponent} from '../oncHistoryUpload/oncHistoryUpload.component'; import {OncHistoryUploadGuard} from '../guards/oncHistoryUpload.guard'; +import { + UsersAndPermissionsCanActivateGuard, + UsersAndPermissionsCanLoadGuard +} from '../guards/usersAndPermissions.guard'; PlotlyModule.plotlyjs = PlotlyJS; @@ -301,7 +305,9 @@ PlotlyModule.plotlyjs = PlotlyJS; Statics, Language, DashboardStatisticsService, - OncHistoryUploadGuard + OncHistoryUploadGuard, + UsersAndPermissionsCanLoadGuard, + UsersAndPermissionsCanActivateGuard ], exports: [RouterModule, MatFormFieldModule, MatInputModule] }) diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.routing.module.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.routing.module.ts index 7e301a99c6..f80fd8df3b 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.routing.module.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/ALL-STUDIES/all-studies.routing.module.ts @@ -38,6 +38,10 @@ import {DashboardStatisticsComponent} from '../dashboard-statistics/dashboard-st import {ScannerComponent} from '../scanner/scanner.component'; import {OncHistoryUploadComponent} from '../oncHistoryUpload/oncHistoryUpload.component'; import {OncHistoryUploadGuard} from '../guards/oncHistoryUpload.guard'; +import { + UsersAndPermissionsCanActivateGuard, + UsersAndPermissionsCanLoadGuard +} from '../guards/usersAndPermissions.guard'; @@ -94,6 +98,12 @@ export const AppRoutes: Routes = [ {path: 'oncHistoryUpload', component: OncHistoryUploadComponent, canActivate: [AuthGuard, OncHistoryUploadGuard]}, {path: 'userSettings', component: UserSettingComponent, canActivate: [AuthGuard]}, + {path: 'usersAndPermissions', + canActivate: [UsersAndPermissionsCanActivateGuard], + canLoad: [UsersAndPermissionsCanLoadGuard], + loadChildren: () => + import('../usersAndPermissions/usersAndPermissions.module').then(m => m.UsersAndPermissionsModule) + }, // Permalink { diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.html similarity index 88% rename from ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.html rename to ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.html index fc1112af0a..36e394d112 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.html +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.html @@ -1,6 +1,6 @@
-

Are you sure you want to delete {{fileName}}?

+

Are you sure you want to delete {{name}}?

Warning: this action can't be undone
diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.scss similarity index 93% rename from ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.scss rename to ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.scss index 3421de1967..3dddce060f 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.scss +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.scss @@ -1,4 +1,4 @@ -@use 'commonStyles' as common; +@use 'projects/ddp-dsm-ui/src/styles/commonStyles' as common; $yes-color: #FF595E; $no-color: #5FA8D3; diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.ts similarity index 80% rename from ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.ts rename to ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.ts index 544e570524..473cf4b293 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.component.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.component.ts @@ -9,14 +9,14 @@ import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; export class ConfirmationModalComponent { constructor( private readonly dialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) private data: {fileName: string}, + @Inject(MAT_DIALOG_DATA) private data: {name: string}, ) {} public confirmationAnswerIs(doIt: boolean): void { this.dialogRef.close(doIt); } - public get fileName(): string { - return this.data.fileName; + public get name(): string { + return this.data.name; } } diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.spec.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.spec.ts similarity index 89% rename from ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.spec.ts rename to ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.spec.ts index 46e554617f..d929b886b7 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/components/confirmationModal/confirmationModal.spec.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/Shared/components/confirmationModal/confirmationModal.spec.ts @@ -19,7 +19,7 @@ describe('ConfirmationModalComponent', () => { }, { provide: MAT_DIALOG_DATA, - useValue: {fileName: 'testFile.pdf'} + useValue: {name: 'testFile.pdf'} } ] }).compileComponents(); @@ -38,12 +38,12 @@ describe('ConfirmationModalComponent', () => { expect(component).toBeTruthy('Component has not been instantiated'); }); - it('should display file name', () => { + it('should display name', () => { const fileName = componentHTML .query(By.css('section.confirmationModal-content p')) .nativeElement.textContent; - expect(fileName).toContain('testFile.pdf', 'File name is not displayed'); + expect(fileName).toContain('testFile.pdf', 'Name is not displayed'); }); }); diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/guards/usersAndPermissions.guard.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/guards/usersAndPermissions.guard.ts new file mode 100644 index 0000000000..029503e77b --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/guards/usersAndPermissions.guard.ts @@ -0,0 +1,30 @@ +import {Injectable} from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivate, + CanLoad, + Route, + RouterStateSnapshot, + UrlSegment, + UrlTree +} from '@angular/router'; +import {RoleService} from '../services/role.service'; + +@Injectable() +export class UsersAndPermissionsCanLoadGuard implements CanLoad { + constructor(private readonly roleService: RoleService) {} + + canLoad(route: Route, segments: UrlSegment[]): boolean | UrlTree { + return this.roleService.isStudyUserAdmin || this.roleService.isPepperAdmin; + } +} + + +@Injectable() +export class UsersAndPermissionsCanActivateGuard implements CanActivate { + constructor(private readonly roleService: RoleService) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree { + return this.roleService.isStudyUserAdmin || this.roleService.isPepperAdmin; + } +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/interceptors/Http-interceptor.service.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/interceptors/Http-interceptor.service.ts index 89f78989ed..a310996256 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/interceptors/Http-interceptor.service.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/interceptors/Http-interceptor.service.ts @@ -9,19 +9,26 @@ import {Observable, throwError} from 'rxjs'; import {catchError} from 'rxjs/operators'; import {Injectable} from '@angular/core'; import {ErrorsService} from '../services/errors.service'; +import {Auth} from '../services/auth.service'; @Injectable() export class HttpInterceptorService implements HttpInterceptor { private readonly ignoreStatuses: number[] = [401]; - constructor(private errorsService: ErrorsService) {} + constructor(private readonly errorsService: ErrorsService, + private readonly authService: Auth) {} intercept(req: HttpRequest, next: HttpHandler): Observable> { return next.handle(req).pipe( catchError((error: any) => { - error instanceof HttpErrorResponse && - !this.ignoreStatuses.includes(error?.status) && - this.errorsService.openSnackbar(error); + + + if(error instanceof HttpErrorResponse) { + !this.ignoreStatuses.includes(error?.status) && + this.errorsService.openSnackbar(error); + + error?.status === 401 && this.authService.doLogout(); + } return throwError(() => error); }) diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/navigation/navigation.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/navigation/navigation.component.html index 401f078c2e..c337176b25 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/navigation/navigation.component.html +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/navigation/navigation.component.html @@ -85,6 +85,7 @@
  • NDI Upload
  • Onc History Upload
  • Drug List
  • +
  • Users And Permissions
  • diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/services/dsm.service.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/services/dsm.service.ts index 36a2681116..ab309c5a50 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/services/dsm.service.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/services/dsm.service.ts @@ -21,6 +21,9 @@ import {IDateRange} from '../dashboard-statistics/interfaces/IDateRange'; import {StatisticsEnum} from '../dashboard-statistics/enums/statistics.enum'; import {SomaticResultSignedUrlRequest} from '../sharedLearningUpload/interfaces/somaticResultSignedUrlRequest'; import {SendToParticipantRequest} from '../sharedLearningUpload/interfaces/sendToParticipant'; +import {AddUsersRequest, RemoveUsersRequest} from '../usersAndPermissions/interfaces/addRemoveUsers'; +import {EditUsers} from '../usersAndPermissions/interfaces/editUsers'; +import {EditUserRoles} from '../usersAndPermissions/interfaces/role'; declare var DDP_ENV: any; @@ -458,6 +461,60 @@ export class DSMService { ); } + public getUsers(realm: string): Observable { + const url = this.baseUrl + DSMService.UI + 'admin/userRole'; + const map: { name: string; value: any }[] = []; + map.push({name: DSMService.REALM, value: realm}); + return this.http.get(url, this.buildQueryHeader(map)).pipe( + catchError(this.handleError) + ); + } + + public addUser(realm: string, addUsers: AddUsersRequest): Observable { + const url = this.baseUrl + DSMService.UI + 'admin/user'; + const map: { name: string; value: any }[] = []; + map.push({name: DSMService.REALM, value: realm}); + return this.http.post(url, addUsers, this.buildQueryHeader(map)).pipe( + catchError(this.handleError) + ); + } + + public removeUser(realm: string, removeUsers: RemoveUsersRequest): Observable { + const url = this.baseUrl + DSMService.UI + 'admin/user'; + const map: { name: string; value: any }[] = []; + map.push({name: DSMService.REALM, value: realm}); + return this.http.post(url, removeUsers, this.buildQueryHeader(map)).pipe( + catchError(this.handleError) + ); + } + + public editUsers(realm: string, editUser: EditUsers): Observable { + const url = this.baseUrl + DSMService.UI + 'admin/user'; + const map: { name: string; value: any }[] = []; + map.push({name: DSMService.REALM, value: realm}); + return this.http.put(url, editUser, this.buildQueryHeader(map)).pipe( + catchError(this.handleError) + ); + } + + public editUsersRoles(realm: string, userRoles: EditUserRoles): Observable { + const url = this.baseUrl + DSMService.UI + 'admin/userRole'; + const map: { name: string; value: any }[] = []; + map.push({name: DSMService.REALM, value: realm}); + return this.http.put(url, userRoles, this.buildQueryHeader(map)).pipe( + catchError(this.handleError) + ); + } + + public availableRoles(realm: string): Observable { + const url = this.baseUrl + DSMService.UI + 'admin/studyRole'; + const map: { name: string; value: any }[] = []; + map.push({name: DSMService.REALM, value: realm}); + return this.http.get(url, this.buildQueryHeader(map)).pipe( + catchError(this.handleError) + ); + } + public getMedicalRecord(participantId: string, institutionId: string): Observable { const url = this.baseUrl + DSMService.UI + 'participant/' + participantId + '/institution/' + institutionId; return this.http.get(url, this.buildHeader()).pipe( diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/services/role.service.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/services/role.service.ts index d2b5e4af98..764ad13621 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/services/role.service.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/services/role.service.ts @@ -41,7 +41,8 @@ export class RoleService { private _user: string; private _userEmail: string; private _userSetting: UserSetting; - + private _studyUserAdmin = false; + private _pepperAdmin = false; constructor( private sessionService: SessionService, @Inject( 'ddp.config' ) private config: ConfigurationService ) { @@ -82,6 +83,8 @@ export class RoleService { this._viewSeqOrderStatus = false; this._uploadRorFile = false; this._viewSharedLearnings = false; + this._studyUserAdmin = false; + this._pepperAdmin = false; } public setRoles( token: string ): void { @@ -188,6 +191,12 @@ export class RoleService { else if (entry === 'view_shared_learnings') { this._viewSharedLearnings = true; } + else if (entry === 'study_user_admin') { + this._studyUserAdmin = true; + } + else if (entry === 'pepper_admin') { + this._pepperAdmin = true; + } } } const userSettings: any = this.getClaimByKeyName( token, 'USER_SETTINGS' ); @@ -311,7 +320,7 @@ export class RoleService { return this._isParticipantEdit; } - private getClaimByKeyName( token: any, key: string ): any { + public getClaimByKeyName( token: any, key: string ): any { return this.sessionService.getDSMClaims( token )[ this.config.auth0ClaimNameSpace + key ]; } @@ -350,4 +359,12 @@ export class RoleService { public get allowViewSharedLearnings(): boolean { return this._viewSharedLearnings; } + + public get isStudyUserAdmin(): boolean { + return this._studyUserAdmin; + } + + public get isPepperAdmin(): boolean { + return this._pepperAdmin; + } } diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/sharedLearningsUpload.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/sharedLearningsUpload.component.ts index 824b53ee49..eaae8008f2 100644 --- a/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/sharedLearningsUpload.component.ts +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/sharedLearningUpload/sharedLearningsUpload.component.ts @@ -15,7 +15,7 @@ import {catchError, filter, finalize, first, take} from 'rxjs/operators'; import {SharedLearningsStateService} from './services/sharedLearningsState.service'; import {MatDialog} from '@angular/material/dialog'; import {RoleService} from '../services/role.service'; -import {ConfirmationModalComponent} from './components/confirmationModal/confirmationModal.component'; +import {ConfirmationModalComponent} from '../Shared/components/confirmationModal/confirmationModal.component'; import {HttpErrorResponse} from '@angular/common/http'; @Component({ @@ -83,7 +83,7 @@ export class SharedLearningsUploadComponent implements OnInit, OnDestroy { public onDeleteFile({somaticDocumentId, fileName}: SomaticResultsFileWithStatus): void { const activeConfirmationDialog = this.matDialog - .open(ConfirmationModalComponent, {data: {fileName}, width: '500px'}); + .open(ConfirmationModalComponent, {data: {name: fileName}, width: '500px'}); activeConfirmationDialog.afterClosed() .pipe( diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.html new file mode 100644 index 0000000000..8134bd0727 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.html @@ -0,0 +1,91 @@ +
    +
    +

    Create User

    +
    + + + +
    + + Email + + + Wrong email + + + Required field + + + + First Name & Last Name + + + Required field + + + + Phone + + + Required field + + +
    + +
    +

    Permissions

    + + +
    +
    + +
    + +
    + +
    +

    Compare permissions to the selected user:

    +
    + + Select User + + + {{email}} + + + + +
    +
    + +
    + + +
    +
    + +
    + + + +
    + +
    +
    diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.scss new file mode 100644 index 0000000000..8bf50c202b --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.scss @@ -0,0 +1,64 @@ +@use "commonStyles" as common; + +.addUserForm { + + &-title { + font-family: Montserrat-SemiBold, sans-serif; + text-align: center; + margin-bottom: 1em; + } + + &-permissions { + overflow: auto; + max-height: 500px; + &-title { + text-align: center; + } + } + + &-buttons { + @include common.flexContainer(center, center); + margin-top: 1em; + &-add { + @include common.flexContainer(center, center); + } + } + + .userData-divider { + margin-top: .7em; + } + + &-compare { + margin-top: .2em; + padding: 1em; + mat-divider { + margin: .5em; + } + + &-input { + &-text { + font-family: Montserrat-SemiBold, sans-serif; + } + &-actions { + @include common.flexContainer(center, center); + &-button { + margin-left: 1em; + } + } + } + + &-roles { + margin: 1em 0; + } + } +} + +.loading-spinner { + @include common.flexContainer(center, center); + padding: 0.5em 0; +} + +app-error-message { + @include common.flexContainer(center, center); + margin-bottom: .7em; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.ts new file mode 100644 index 0000000000..1df4bf446c --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/addUser/addUser.component.ts @@ -0,0 +1,103 @@ +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject} from '@angular/core'; +import {FormBuilder, Validators} from '@angular/forms'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Role} from '../../interfaces/role'; +import {AddUser, AddUserModal} from '../../interfaces/addRemoveUsers'; +import {MatSelectChange} from '@angular/material/select'; +import {cloneDeep} from 'lodash'; +import {UsersAndPermissionsStateService} from '../../services/usersAndPermissionsState.service'; +import {Subject, takeUntil} from 'rxjs'; +import {HttpErrorResponse} from '@angular/common/http'; +import {finalize} from 'rxjs/operators'; + +@Component({ + selector: 'app-add-administration-user', + templateUrl: 'addUser.component.html', + styleUrls: ['addUser.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AddUserComponent { + public availableRoles = cloneDeep(this.data.availableRoles).map(role => ({...role, hasRole: false})); + public selectedUserRoles: Role[]; + public errorMessage: string | null = null; + public isLoading = false; + + public readonly addUserForm = this.formBuilder.group({ + email: [null, [Validators.required, Validators.email]], + name: [null, Validators.required], + phone: [null, Validators.required], + }); + + private onlySelectedRoles: Role[] = []; + private subscriptionSubject$ = new Subject(); + + constructor( + @Inject(MAT_DIALOG_DATA) private data: AddUserModal, + private readonly matDialogRef: MatDialogRef, + private readonly formBuilder: FormBuilder, + private readonly stateService: UsersAndPermissionsStateService, + private readonly cdr: ChangeDetectorRef + ) {} + + public addUser(): void { + if (this.allowAddingUser) { + this.isLoading = true; + this.errorMessage = null; + const userToAdd: AddUser = { + ...this.addUserForm.getRawValue(), + roles: this.onlySelectedRoles.map(r => r.name) + }; + this.stateService.addUser(userToAdd) + .pipe( + takeUntil(this.subscriptionSubject$), + finalize(() => { + this.cdr.markForCheck(); + this.isLoading = false; + }) + ) + .subscribe({ + next: () => this.matDialogRef.close(userToAdd), + error: (error) => this.handleError(error) + }); + } + } + + + public get allowAddingUser(): boolean { + return this.addUserForm.valid && !!this.onlySelectedRoles?.length; + } + + public roleSelected(role: Role): void { + const foundRoleIndex = this.onlySelectedRoles.findIndex(r => r.name === role.name); + if(foundRoleIndex > -1 && role.hasRole) { + this.onlySelectedRoles[foundRoleIndex] = role; + } else if (foundRoleIndex === -1 && role.hasRole) { + this.onlySelectedRoles.push(role); + } else if (foundRoleIndex > -1 && !role.hasRole) { + this.onlySelectedRoles.splice(foundRoleIndex, 1); + } + } + + public get existingUsersEmails(): string[] { + return this.data.existingUsers.map(user => user.email); + } + + public userSelected({value}: MatSelectChange): void { + this.selectedUserRoles = this.data.existingUsers.find(({email}) => email === value).roles; + } + + public applyRoles(): void { + this.availableRoles = this.availableRoles.map((role: Role) => ({ + ...role, + hasRole: this.selectedUserRoles.find(r => r.name === role.name)?.hasRole || false + })) as any; + + this.onlySelectedRoles = this.availableRoles.filter(r => r.hasRole); + } + + private handleError(error: any): void { + if (error instanceof HttpErrorResponse) { + this.errorMessage = error.error; + } + } +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.html new file mode 100644 index 0000000000..9cd1a3e217 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.html @@ -0,0 +1,53 @@ +
    +
    +

    Compare 2 Users

    +
    + + + + + + + + + + + + + + + + + + + +
    Role {{role.displayText}} {{firstUser.email}} + + + + + + + + + + + + +
    + + + + + + {{secondUser?.email || 'Select User Email'}} + + {{userEmail}} + + + +
    diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.scss new file mode 100644 index 0000000000..3339f6628c --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.scss @@ -0,0 +1,19 @@ +.compare-container { + + &-title { + font-family: Montserrat-SemiBold, sans-serif; + text-align: center; + margin: 1em 0; + h3 { + margin: 0; + } + } + + table { + width: 100%; + } + + th.mat-header-cell { + text-align: center!important; + } +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.ts new file mode 100644 index 0000000000..27c24d822b --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/comparePermissions/comparePermissions.component.ts @@ -0,0 +1,56 @@ +import {Component, Inject, OnInit} from '@angular/core'; +import {MAT_DIALOG_DATA} from '@angular/material/dialog'; +import {User} from '../../interfaces/user'; +import {MatSelectChange} from '@angular/material/select'; +import {Role} from '../../interfaces/role'; + +@Component({ + selector: 'app-compare-permissions', + templateUrl: 'comparePermissions.component.html', + styleUrls: ['comparePermissions.component.scss'] +}) +export class ComparePermissionsComponent implements OnInit { + public displayedColumns = ['name', this.firstUser.email, 'selectUser']; + public roles = []; + public secondUser: User; + + constructor(@Inject(MAT_DIALOG_DATA) + private data: {firstUser: User; allUsers: User[]}) {} + + ngOnInit(): void { + this.roles = [...this.firstUser.roles]; + } + + public userSelected({value: selectedUserEmail}: MatSelectChange): void { + const foundUser = this.data.allUsers.find(({email}) => email === selectedUserEmail); + this.displayedColumns = ['name', this.firstUser.email]; + this.displayedColumns.push(foundUser.email); + this.roles = this.firstUser.roles; + + for(const role of foundUser.roles) { + if(this.roles.findIndex(r => r.name === role.name) === -1) { + this.roles.push(role); + } + } + + this.secondUser = foundUser; + } + + public get firstUser(): User { + return this.data.firstUser; + } + + public get allUsers(): string[] { + return this.data.allUsers.filter(user => user.email !== this.firstUser.email) + .map((user) => user.email); + } + + public findRoleForFirstUser(roleName: string): Role { + return this.firstUser.roles.find(role => role.name === roleName); + } + + public findRoleForSecondUser(roleName: string): Role { + return this.secondUser.roles.find(role => role.name === roleName); + } + +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/error-message/error-message.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/error-message/error-message.component.ts new file mode 100644 index 0000000000..f2f728a424 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/error-message/error-message.component.ts @@ -0,0 +1,43 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; + +@Component({ + selector: 'app-error-message', + template: ` +
    +

    {{errorText}}

    + + +

    Error message:
    {{errorMessage}}

    +
    +
    + `, + styles: [` + p { + margin: 0; + padding: 0; + } + mat-divider { + margin: .5em 0; + } + .error { + width: 50%; + margin: .5em; + background-color: rgba(230, 57, 70, 0.49); + border-radius: 1em; + padding: 1em; + text-align: center; + font-family: Montserrat-SemiBold, sans-serif; + } + .error-message { + font-family: Montserrat-Regular, sans-serif; + } + .error-message-text { + font-family: Montserrat-SemiBold, sans-serif; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ErrorMessageComponent { + @Input() errorText: string; + @Input() errorMessage: string; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.html new file mode 100644 index 0000000000..6558ac899a --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.html @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.scss new file mode 100644 index 0000000000..ce1997ad3d --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.scss @@ -0,0 +1,8 @@ +@use "commonStyles" as common; + + + +app-error-message { + @include common.flexContainer(center, center); + margin-bottom: .7em; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.ts new file mode 100644 index 0000000000..17bfd1197f --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/listUsers/listUsers.component.ts @@ -0,0 +1,39 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {User} from '../../interfaces/user'; +import {MatDialog} from '@angular/material/dialog'; +import {ComparePermissionsComponent} from '../comparePermissions/comparePermissions.component'; +import {ErrorUi} from '../../interfaces/error-ui'; + +@Component({ + selector: 'app-list-users', + templateUrl: 'listUsers.component.html', + styleUrls: ['listUsers.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ListUsersComponent { + @Input() usersList: User[]; + public displayError = false; + public errorMessage: string | null = null; + public errorText: string | null = null; + + constructor(private readonly matDialog: MatDialog) { + } + + public trackBy(index: number, {name, phone, email}: User): any { + return `${name}_${phone}_${email}`; + } + + public openPermissionsComparisonModal(firstUser: User): void { + this.matDialog.open(ComparePermissionsComponent, {data: { + firstUser, + allUsers: this.usersList + }, maxHeight: '50em', width: '70%'}); + } + + public onError({displayError, errorText, errorMessage}: ErrorUi): void { + this.displayError = displayError; + this.errorText = errorText; + this.errorMessage = errorMessage; + } + +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.html new file mode 100644 index 0000000000..2ca058c328 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.html @@ -0,0 +1,19 @@ +
    + + + + + + + +
    diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.scss new file mode 100644 index 0000000000..4082b5c0b6 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.scss @@ -0,0 +1,144 @@ +@use "commonStyles" as common; + +.checkbox-container { + * { + box-sizing: border-box; + } + + .cbx { + @include common.flexContainer; + -webkit-user-select: none; + user-select: none; + cursor: pointer; + padding: 6px 8px; + border-radius: 6px; + overflow: hidden; + transition: all 0.2s ease; + + &:not(:last-child) { + margin-right: 6px; + } + + &:hover { + background: rgba(0, 119, 255, 0.06); + } + + &.labelDisabled:hover { + background: none; + cursor: default; + } + + span { + float: left; + vertical-align: middle; + transform: translate3d(0, 0, 0); + } + + span:first-child { + position: relative; + min-width: 18px; + max-width: 20%; + width: 18px; + height: 18px; + border-radius: 4px; + transform: scale(1); + border: 1px solid #cccfdb; + transition: all 0.2s ease; + box-shadow: 0 1px 1px rgba(0, 16, 75, 0.05); + } + + span:first-child svg { + max-width: 100%; + max-height: 100%; + position: absolute; + top: 3px; + left: 2px; + fill: none; + stroke: #fff; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 16px; + stroke-dashoffset: 16px; + transition: all 0.3s ease; + transition-delay: 0.1s; + transform: translate3d(0, 0, 0); + } + + span:last-child { + max-width: 90%; + display: inline-block; + padding-left: 8px; + line-height: 18px; + } + + &:hover span:first-child { + border-color: #66bb6a; + } + + } + + .inp-cbx { + position: absolute; + visibility: hidden; + + &:checked + .cbx span:first-child { + background: #66bb6a; + border-color: #66bb6a; + animation: wave-4 0.4s ease; + } + + &:checked:disabled + .cbx span:first-child { + background: rgba(102, 187, 106, 0.57); + border-color: rgba(102, 187, 106, 0.53); + } + + &:checked + .cbx span:first-child svg { + stroke-dashoffset: 0; + } + } + + .inline-svg { + position: absolute; + width: 0; + height: 0; + pointer-events: none; + user-select: none; + } + + @media screen and (max-width: 640px) { + .checkbox-container .cbx { + width: 100%; + display: inline-block; + } + } + + @-moz-keyframes wave-4 { + 50% { + transform: scale(0.9); + } + } + + @-webkit-keyframes wave-4 { + 50% { + transform: scale(0.9); + } + } + + @-o-keyframes wave-4 { + 50% { + transform: scale(0.9); + } + } + + @keyframes wave-4 { + 50% { + transform: scale(0.9); + } + } + +} + + + + diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.ts new file mode 100644 index 0000000000..2b1b2de746 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/permissionCheckbox/permissionCheckbox.component.ts @@ -0,0 +1,29 @@ +import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {Role} from '../../interfaces/role'; + +@Component({ + selector: 'app-permission-checkbox', + templateUrl: 'permissionCheckbox.component.html', + styleUrls: ['permissionCheckbox.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PermissionCheckboxComponent implements OnInit { + @Input() role: Role; + @Input() onlyCheckbox = false; + @Input() isDisabled = false; + + @Output() checkboxChanged = new EventEmitter(); + + public idRandomizer: string; + + ngOnInit(): void { + this.idRandomizer = + (this.role?.name || 'defaultGuid1994') + + Math.floor(Math.random() * 100000); + } + + public onChange(changeEvent: Event, role: Role): void { + const {checked} = changeEvent.target as HTMLInputElement; + this.checkboxChanged.next({...role, hasRole: checked}); + } +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.html new file mode 100644 index 0000000000..d3bde7b0c9 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.html @@ -0,0 +1,117 @@ + + + + + {{user.email}} + + +
    +
    +
    +

    Name: + {{user.name}} + +

    +

    Phone: + {{user.phone}} + +

    +
    + +
    +
    + + + + + +
    +
    +
    +
    + +
    +

    Permissions

    +
    + + + +
    +
    + + + + +
    +
    + + + +
    + + +
    + +
    +
    + + + + +
    + +
    +
    diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.scss new file mode 100644 index 0000000000..434f812d00 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.scss @@ -0,0 +1,159 @@ +@use "commonStyles" as common; + +.mat-accordion .mat-expansion-panel { + margin-bottom: 10px; +} + +.mat-expansion-panel-spacing { + margin: 1em; +} + +mat-expansion-panel-header { + padding: 2em 18px; +} + +p { + margin: 0; + padding: 0; +} + +mat-panel-title { + align-items: center; + font-family: Montserrat-SemiBold, sans-serif; + flex-grow: 0; + width: 30%; + overflow: auto; +} + +mat-panel-description { + align-items: center; + flex-grow: 1; + + .header { + @include common.flexContainer(normal, space-between); + width: 100%; + height: auto; + + button:disabled { + mat-icon { + color: grey; + } + } + + &-text { + @include common.flexContainer(center); + + width: 55%; + &-inputs { + &-name, &-phone { + overflow: auto; + width: 2em; + max-width: 100%; + font-weight: bold; + + span { + font-family: Montserrat-SemiBold, sans-serif; + } + } + + &-editing-name, &-editing-phone { + border: none; + background-color: inherit; + border-bottom: 1px solid #003049; + outline: none; + + &:focus-within { + border-bottom: 1px solid rgba(0, 48, 73, 0.49); + } + } + } + + &-saveButton { + @include common.flexContainer(center, center); + + mat-icon { + color: #386641; + + &:hover { + color: rgba(56, 102, 65, 0.25); + } + } + } + } + + &-buttons { + @include common.flexContainer(normal, flex-end); + width: 45%; + + &-compare { + mat-icon { + color: #003049; + + &:hover { + color: rgba(0, 48, 73, 0.25); + } + } + } + + &-edit { + mat-icon { + color: #fcbf49; + + &:hover { + color: rgba(252, 191, 73, 0.25); + } + } + } + + &-delete { + mat-icon { + color: #d62828; + + &:hover { + color: rgba(214, 40, 40, 0.25); + } + } + } + } + } +} + +.title { + text-align: center; + font-family: Montserrat-SemiBold, sans-serif; +} + +.permissions { + width: 100%; + + &-selectAll { + @include common.flexContainer(center, center); + } + + &-selectSingle { + @include common.flexContainer; + flex-wrap: wrap; + padding: 1em; + + + app-permission-checkbox { + flex-basis: 20%; + max-width: 20%; + } + } +} + +.actionButtons { + @include common.flexContainer(normal, center); + margin-top: .7em; + + &-save { + @include common.flexContainer(center, center); + margin-right: .7em; + } +} + +.spinner-container { + @include common.flexContainer(center, center); +} + diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.ts new file mode 100644 index 0000000000..8925d66b67 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/components/user/administrationUser.component.ts @@ -0,0 +1,215 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, EventEmitter, + Input, OnDestroy, OnInit, Output +} from '@angular/core'; +import {User} from '../../interfaces/user'; +import {Role} from '../../interfaces/role'; +import {cloneDeep} from 'lodash'; +import {RoleService} from '../../../services/role.service'; +import {FormBuilder, FormGroup} from '@angular/forms'; +import {UsersAndPermissionsStateService} from '../../services/usersAndPermissionsState.service'; +import {RemoveUsersRequest} from '../../interfaces/addRemoveUsers'; +import {mergeMap, Subject, takeUntil, tap} from 'rxjs'; +import {filter, finalize, take} from 'rxjs/operators'; +import {EditUsers} from '../../interfaces/editUsers'; +import {HttpErrorResponse} from '@angular/common/http'; +import {ErrorUi} from '../../interfaces/error-ui'; +import {ConfirmationModalComponent} from '../../../Shared/components/confirmationModal/confirmationModal.component'; +import {MatDialog} from '@angular/material/dialog'; + +@Component({ + selector: 'app-administration-user', + templateUrl: 'administrationUser.component.html', + styleUrls: ['administrationUser.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class AdministrationUserComponent implements OnInit, OnDestroy { + public editUserForm: FormGroup; + + public user: User; + private rolesBeforeChange: Role[]; + private subscriptionSubject$ = new Subject(); + + /* Switchers */ + public arePermissionActionButtonsDisabled = true; + public isUserEditing = false; + public isEditUserLoading = false; + public isEditPermissionsLoading = false; + public isDeleteUserLoading = false; + + @Input('user') set administrationUser(user: User) { + this.user = user; + this.rolesBeforeChange = cloneDeep(user.roles); + } + + @Output() comparingUser = new EventEmitter(); + @Output() reportError = new EventEmitter>(); + + constructor(private readonly cdr: ChangeDetectorRef, + private readonly roleService: RoleService, + private readonly formBuilder: FormBuilder, + private readonly stateService: UsersAndPermissionsStateService, + private readonly matDialog: MatDialog) { + } + + ngOnInit(): void { + this.editUserForm = this.formBuilder.group({ + name: [this.user.name], + phone: [this.user.phone] + }); + } + + ngOnDestroy(): void { + this.subscriptionSubject$.next(); + this.subscriptionSubject$.complete(); + } + + /* Event Handlers */ + + public compareUser(event: Event): void { + event.stopPropagation(); + this.comparingUser.emit(this.user); + } + + public editUser(event: Event): void { + event.stopPropagation(); + this.isUserEditing = !this.isUserEditing; + } + + public saveEditedUser(event: Event): void { + event.stopPropagation(); + this.isEditUserLoading = true; + this.reportError.emit({displayError: false}); + this.editUserForm.disable(); + const userToEdit: EditUsers = { + users: [ + {email: this.user.email, ...this.editUserForm.getRawValue()} + ] + }; + + this.stateService.editUsers(userToEdit) + .pipe( + takeUntil(this.subscriptionSubject$), + finalize(() => { + this.cdr.markForCheck(); + this.isEditUserLoading = false; + this.editUserForm.enable(); + }) + ) + .subscribe({ + error: (error) => this.handleError(error, `Couldn't edit the user - ${this.user.email}`) + }); + } + + public removeUser(event: Event): void { + event.stopPropagation(); + this.reportError.emit({displayError: false}); + + const usersToRemove: RemoveUsersRequest = { + removeUsers: [this.user.email] + }; + + const activeConfirmationDialog = this.matDialog + .open(ConfirmationModalComponent, {data: {name: this.user.email}, width: '500px'}); + + activeConfirmationDialog.afterClosed() + .pipe( + filter((answer: boolean) => answer), + tap(() => { + this.cdr.markForCheck(); + this.isDeleteUserLoading = true; + }), + mergeMap(() => this.stateService.removeUsers(usersToRemove) + .pipe( + finalize(() => { + this.cdr.markForCheck(); + this.isDeleteUserLoading = false; + }) + )), + take(1), + takeUntil(this.subscriptionSubject$) + ).subscribe({ + error: (error) => this.handleError(error, `Couldn't remove the user - ${this.user.email}`) + }); + } + + public onCheckboxChanged(changedRole: Role): void { + this.user.roles = this.user.roles.map((role) => + changedRole.name === role.name ? + {...role, hasRole: changedRole.hasRole} : + role + ); + + this.changeActionButtonsState(!this.hasPermissionsChanged); + } + + public saveChanges(): void { + this.isEditPermissionsLoading = true; + this.reportError.emit({displayError: false}); + + const userRolesToEdit = { + users: [this.user.email], + roles: this.user.roles.filter(role => role.hasRole).map(role => role.name) + }; + + this.stateService.editUserRoles(userRolesToEdit) + .pipe( + takeUntil(this.subscriptionSubject$), + finalize(() => { + this.cdr.markForCheck(); + this.isEditPermissionsLoading = false; + }) + ) + .subscribe({ + next: () => { + this.rolesBeforeChange = this.user.roles; + this.arePermissionActionButtonsDisabled = true; + }, + error: (error) => this.handleError(error, `Couldn't update roles for the user - ${this.user.email}`) + }); + } + + public discardChanges(): void { + this.user.roles = this.rolesBeforeChange; + this.changeActionButtonsState(true); + } + + /* Template methods */ + + public get doNotAllowCollapse(): boolean { + return this.hasPermissionsChanged || this.disableUserActionButtons; + } + + public get disableUserActionButtons(): boolean { + return this.isEditUserLoading || this.isDeleteUserLoading || this.isEditPermissionsLoading; + } + + public get activeUserEmail(): string { + return this.roleService.userMail(); + } + + + /* Helper functions */ + + private get hasPermissionsChanged(): boolean { + return !this.rolesBeforeChange?.every(({hasRole, name}) => + hasRole === this.user.roles.find(({name: name2}) => name === name2).hasRole); + } + + private changeActionButtonsState(isDisabled: boolean): void { + this.arePermissionActionButtonsDisabled = isDisabled; + } + + private handleError(error: any, text: string): void { + if (error instanceof HttpErrorResponse) { + this.reportError.emit({ + displayError: true, + errorText: text, + errorMessage: error.error + }); + } + } + +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/addRemoveUsers.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/addRemoveUsers.ts new file mode 100644 index 0000000000..66e0a8b929 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/addRemoveUsers.ts @@ -0,0 +1,22 @@ +import {Role} from './role'; +import {User} from './user'; + +export interface AddUser { + email: string; + name: string; + phone: string; + roles: string[]; +} + +export interface AddUsersRequest { + addUsers: AddUser[]; +} + +export interface RemoveUsersRequest { + removeUsers: string[]; // users emails +} + +export interface AddUserModal { + availableRoles: Partial; + existingUsers: User[]; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/editUsers.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/editUsers.ts new file mode 100644 index 0000000000..0eb0eb4ae0 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/editUsers.ts @@ -0,0 +1,5 @@ +import {User} from './user'; + +export interface EditUsers { + users: Partial[]; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/error-ui.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/error-ui.ts new file mode 100644 index 0000000000..e2e171c757 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/error-ui.ts @@ -0,0 +1,5 @@ +export interface ErrorUi { + displayError: boolean; + errorText: string; + errorMessage: string; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/role.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/role.ts new file mode 100644 index 0000000000..9fe812b89a --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/role.ts @@ -0,0 +1,19 @@ +export interface Role { + name: string; + displayText: string; + hasRole: boolean; +} + +export interface EditUserRoles { + users: string[]; + roles: string[]; +} + +export interface AvailableRole { + name: string; + displayText: string; +} + +export interface AvailableStudyRolesResponse { + roles: AvailableRole[]; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/user.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/user.ts new file mode 100644 index 0000000000..fda071db21 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/interfaces/user.ts @@ -0,0 +1,12 @@ +import {Role} from './role'; + +export interface User { + email: string; + name: string; + phone: string; + roles: Role[]; +} + +export interface AdministrationUsersResponse { + users: User[]; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/services/usersAndPermissionsHttp.service.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/services/usersAndPermissionsHttp.service.ts new file mode 100644 index 0000000000..39f5236fe8 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/services/usersAndPermissionsHttp.service.ts @@ -0,0 +1,44 @@ +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {User, AdministrationUsersResponse} from '../interfaces/user'; +import {DSMService} from '../../services/dsm.service'; +import {SessionService} from '../../services/session.service'; +import {AddUsersRequest, RemoveUsersRequest} from '../interfaces/addRemoveUsers'; +import {AvailableStudyRolesResponse, EditUserRoles} from '../interfaces/role'; +import {EditUsers} from '../interfaces/editUsers'; + +@Injectable() +export class UsersAndPermissionsHttpService { + + constructor(private readonly dsmService: DSMService, + private readonly sessionService: SessionService) { + } + + public get users(): Observable { + return this.dsmService.getUsers(this.realm); + } + + public get studyRoles(): Observable { + return this.dsmService.availableRoles(this.realm); + } + + public addUsers(addUsers: AddUsersRequest): Observable { + return this.dsmService.addUser(this.realm, addUsers); + } + + public editUserRoles(userRoles: EditUserRoles): Observable { + return this.dsmService.editUsersRoles(this.realm, userRoles); + } + + public editUsers(editUsers: EditUsers): Observable { + return this.dsmService.editUsers(this.realm, editUsers); + } + + public removeUsers(removeUsers: RemoveUsersRequest): Observable { + return this.dsmService.removeUser(this.realm, removeUsers); + } + + private get realm(): string { + return this.sessionService.selectedRealm; + } +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/services/usersAndPermissionsState.service.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/services/usersAndPermissionsState.service.ts new file mode 100644 index 0000000000..634d035a27 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/services/usersAndPermissionsState.service.ts @@ -0,0 +1,110 @@ +import {Injectable} from '@angular/core'; +import {UsersAndPermissionsHttpService} from './usersAndPermissionsHttp.service'; +import {BehaviorSubject, mergeMap, Observable, tap} from 'rxjs'; +import {map, pluck} from 'rxjs/operators'; +import {AddUser, RemoveUsersRequest} from '../interfaces/addRemoveUsers'; +import {User} from '../interfaces/user'; +import {AvailableRole, EditUserRoles, Role} from '../interfaces/role'; +import {EditUsers} from '../interfaces/editUsers'; + +@Injectable() +export class UsersAndPermissionsStateService { + private readonly usersListSubject$ = new BehaviorSubject([]); + private readonly availableRolesSubject$ = new BehaviorSubject([]); + + public readonly usersList$ = this.usersListSubject$.asObservable() + .pipe( + map((users: User[]) => + users.map((user: User) => + ({ + ...user, + roles: this.sortRoles(user.roles.slice()) + }) + ) + ) + ); + + public readonly availableRoles$ = this.availableRolesSubject$.asObservable() + .pipe( + map((roles: Role[]) => this.sortRoles(roles)) + ); + + + constructor(private readonly httpService: UsersAndPermissionsHttpService) { + } + + + /* Events */ + + public initData(): Observable { + return this.httpService.users.pipe( + pluck('users'), + tap(users => this.usersListSubject$.next(users)), + mergeMap(() => this.httpService.studyRoles + .pipe( + pluck('roles'), + tap(roles => this.availableRolesSubject$.next(roles)) + )) + ); + } + + public addUser(user: AddUser): Observable { + return this.httpService.addUsers({addUsers: [user]}) + .pipe(tap(() => this.pushUser(user))); + } + + public editUserRoles(userRoles: EditUserRoles): Observable { + return this.httpService.editUserRoles(userRoles); + } + + public removeUsers(removeUsers: RemoveUsersRequest): Observable { + return this.httpService.removeUsers(removeUsers) + .pipe( + tap(() => this.removeUser(removeUsers.removeUsers)) + ); + } + + public editUsers(editUsers: EditUsers): Observable { + return this.httpService.editUsers(editUsers) + .pipe(tap(() => this.editUser(editUsers.users[0]))); + } + + /* Helper functions */ + + private pushUser(newUser: AddUser): void { + this.usersListSubject$.next( + [...this.usersListSubject$.getValue(), + { + ...newUser, + roles: [ + ...this.availableRolesSubject$.getValue() + .map(role => ({ + ...role, + hasRole: !!newUser.roles.find(r => r === role.name) + })) + ] + }] + ); + } + + private editUser(editedUser: Partial): void { + this.usersListSubject$.next(this.usersListSubject$.getValue().map((user) => { + if (editedUser.email === user.email) { + user.name = editedUser.name; + user.phone = editedUser.phone; + } + return user; + })); + } + + + private removeUser(removedUsersEmails: string[]): void { + this.usersListSubject$ + .next(this.usersListSubject$.getValue().filter(user => !removedUsersEmails.includes(user.email))); + } + + private sortRoles(roles: Role[] | AvailableRole[]): Role[] | AvailableRole[] { + return roles.sort(({displayText: role1}, {displayText: role2}) => + role1.localeCompare(role2)); + } +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.html b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.html new file mode 100644 index 0000000000..0de2ec5e8c --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.html @@ -0,0 +1,24 @@ +
    +

    Users And Permissions

    +
    + + + + + + + + + + + + + + + + +
    + +
    +
    diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.scss b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.scss new file mode 100644 index 0000000000..ab6315ee9a --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.scss @@ -0,0 +1,19 @@ +@use "commonStyles" as common; + +.title { + text-align: center; + margin-bottom: 2em; +} + +.addUserButton { + margin-bottom: 1.5em; +} + +.loading-spinner { + @include common.flexContainer(normal, center); +} + +app-error-message { + @include common.flexContainer(center, center); + margin-bottom: .7em; +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.ts new file mode 100644 index 0000000000..12242aa9c0 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.component.ts @@ -0,0 +1,71 @@ +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit} from '@angular/core'; +import {Observable, Subject, takeUntil} from 'rxjs'; +import {finalize} from 'rxjs/operators'; +import {UsersAndPermissionsStateService} from './services/usersAndPermissionsState.service'; +import {HttpErrorResponse} from '@angular/common/http'; +import {AddUserComponent} from './components/addUser/addUser.component'; +import {MatDialog} from '@angular/material/dialog'; +import {User} from './interfaces/user'; +import {Role} from './interfaces/role'; + +@Component({ + selector: 'app-users-and-permissions', + templateUrl: 'usersAndPermissions.component.html', + styleUrls: ['usersAndPermissions.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class UsersAndPermissionsComponent implements OnDestroy, OnInit { + public usersList$ = this.stateService.usersList$; + public availableRoles$ = this.stateService.availableRoles$; + + public isLoading = false; + public errorMessage: string | null = null; + + private readonly subscriptionSubject$ = new Subject(); + + constructor( + private readonly stateService: UsersAndPermissionsStateService, + private readonly cdr: ChangeDetectorRef, + private readonly matDialog: MatDialog) { + } + + ngOnInit(): void { + this.initData() + .pipe(takeUntil(this.subscriptionSubject$)) + .subscribe({ + error: (error) => this.handleError(error) + }); + } + + ngOnDestroy(): void { + this.subscriptionSubject$.next(); + this.subscriptionSubject$.complete(); + } + + public onAddUser(usersList: User[], availableRoles: Role[]): void { + this.matDialog.open(AddUserComponent, {data:{ + existingUsers: usersList, + availableRoles: availableRoles, + }, height: '95%'}); + } + + private initData(): Observable { + this.isLoading = true; + this.errorMessage = null; + return this.stateService.initData() + .pipe( + finalize(() => { + this.isLoading = false; + this.cdr.markForCheck(); + }) + ); + } + + private handleError(error: any): void { + if(error instanceof HttpErrorResponse) { + this.errorMessage = error.error; + } + } + + +} diff --git a/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.module.ts b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.module.ts new file mode 100644 index 0000000000..3406c72385 --- /dev/null +++ b/ddp-workspace/projects/ddp-dsm-ui/src/app/usersAndPermissions/usersAndPermissions.module.ts @@ -0,0 +1,68 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {RouterModule, Routes} from '@angular/router'; +import {UsersAndPermissionsComponent} from './usersAndPermissions.component'; +import {ListUsersComponent} from './components/listUsers/listUsers.component'; +import {MatExpansionModule} from '@angular/material/expansion'; +import {MatIconModule} from '@angular/material/icon'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {PermissionCheckboxComponent} from './components/permissionCheckbox/permissionCheckbox.component'; +import {MatDividerModule} from '@angular/material/divider'; +import {MatTooltipModule} from '@angular/material/tooltip'; +import {AdministrationUserComponent} from './components/user/administrationUser.component'; +import {MatInputModule} from '@angular/material/input'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; +import {ComparePermissionsComponent} from './components/comparePermissions/comparePermissions.component'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatTableModule} from '@angular/material/table'; +import {MatSelectModule} from '@angular/material/select'; +import {AddUserComponent} from './components/addUser/addUser.component'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatAutocompleteModule} from '@angular/material/autocomplete'; +import {ErrorMessageComponent} from './components/error-message/error-message.component'; +import {UsersAndPermissionsHttpService} from './services/usersAndPermissionsHttp.service'; +import {UsersAndPermissionsStateService} from './services/usersAndPermissionsState.service'; + +const routes: Routes = [ + {path: '', component: UsersAndPermissionsComponent} +]; + +@NgModule({ + declarations: [ + UsersAndPermissionsComponent, + ListUsersComponent, + PermissionCheckboxComponent, + AdministrationUserComponent, + ComparePermissionsComponent, + AddUserComponent, + ErrorMessageComponent + ], + imports: [ + CommonModule, + RouterModule.forChild(routes), + ReactiveFormsModule, + MatFormFieldModule, + MatExpansionModule, + MatIconModule, + MatButtonModule, + MatCheckboxModule, + MatDividerModule, + MatTooltipModule, + MatInputModule, + FormsModule, + MatProgressSpinnerModule, + MatDialogModule, + MatTableModule, + MatSelectModule, + ReactiveFormsModule, + MatAutocompleteModule + ], + providers: [ + UsersAndPermissionsHttpService, + UsersAndPermissionsStateService + ] +}) +export class UsersAndPermissionsModule { +}