diff --git a/apps/red-ui/src/app/modules/admin/admin.module.ts b/apps/red-ui/src/app/modules/admin/admin.module.ts index 62f42167a..2005979f5 100644 --- a/apps/red-ui/src/app/modules/admin/admin.module.ts +++ b/apps/red-ui/src/app/modules/admin/admin.module.ts @@ -30,6 +30,7 @@ import { SmtpConfigScreenComponent } from './screens/smtp-config/smtp-config-scr import { SmtpAuthDialogComponent } from './dialogs/smtp-auth-dialog/smtp-auth-dialog.component'; import { AddEditUserDialogComponent } from './dialogs/add-edit-user-dialog/add-edit-user-dialog.component'; import { ConfirmDeleteUserDialogComponent } from './dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component'; +import { UsersStatsComponent } from './components/users-stats/users-stats.component'; const dialogs = [ AddEditRuleSetDialogComponent, @@ -63,6 +64,7 @@ const components = [ TabsComponent, ComboChartComponent, ComboSeriesVerticalComponent, + UsersStatsComponent, ...dialogs, ...screens ]; diff --git a/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html new file mode 100644 index 000000000..36a3114e6 --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.html @@ -0,0 +1,25 @@ +
+ +
+
+ +
+
+ +
+ +
+ +
diff --git a/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss new file mode 100644 index 000000000..8938e46bf --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.scss @@ -0,0 +1,19 @@ +.header-wrapper { + display: flex; + flex-direction: row; + position: relative; + + .heading-xl { + max-width: 88%; + } + + redaction-circle-button { + position: absolute; + top: -8px; + left: 270px; + } +} + +.mt-44 { + margin-top: 44px; +} diff --git a/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts new file mode 100644 index 000000000..79a35e9ed --- /dev/null +++ b/apps/red-ui/src/app/modules/admin/components/users-stats/users-stats.component.ts @@ -0,0 +1,16 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; + +@Component({ + selector: 'redaction-users-stats', + templateUrl: './users-stats.component.html', + styleUrls: ['./users-stats.component.scss'] +}) +export class UsersStatsComponent implements OnInit { + @Output() public toggleCollapse = new EventEmitter(); + @Input() chartData: DoughnutChartConfig[]; + + constructor() {} + + ngOnInit(): void {} +} diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html index f06139aa1..742318f8e 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.html @@ -28,13 +28,7 @@
- + {{ 'roles.' + role | translate }}
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts index dd439a765..91add8a27 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/add-edit-user-dialog/add-edit-user-dialog.component.ts @@ -1,10 +1,8 @@ import { Component, Inject } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { User, UserControllerService } from '@redaction/red-ui-http'; +import { User } from '@redaction/red-ui-http'; import { UserService } from '../../../../services/user.service'; -import { MatCheckboxChange } from '@angular/material/checkbox'; -import { AdminDialogService } from '../../services/admin-dialog-service.service'; @Component({ selector: 'redaction-add-edit-user-dialog', @@ -17,19 +15,39 @@ export class AddEditUserDialogComponent { constructor( private readonly _formBuilder: FormBuilder, - private readonly _userControllerService: UserControllerService, private readonly _userService: UserService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public user: User ) { + const rolesControls = this.ROLES.reduce( + (prev, role) => ({ + ...prev, + [role]: [this.user && this.user.roles.indexOf(role) !== -1] + }), + {} + ); this.userForm = this._formBuilder.group({ firstName: [this.user?.firstName, Validators.required], lastName: [this.user?.lastName, Validators.required], email: [{ value: this.user?.email, disabled: !!user }, [Validators.required, Validators.email]], // password: [this.user?.password, Validators.required], - admin: [user && this._userService.isAdmin(user)], - roles: [user?.roles || []] + ...rolesControls }); + this._setRolesRequirements(); + } + + private _setRolesRequirements() { + const requirements = { RED_MANAGER: 'RED_USER', RED_ADMIN: 'RED_USER_ADMIN' }; + for (const key of Object.keys(requirements)) { + this.userForm.controls[key].valueChanges.subscribe((checked) => { + if (checked) { + this.userForm.patchValue({ [requirements[key]]: true }); + this.userForm.controls[requirements[key]].disable(); + } else { + this.userForm.controls[requirements[key]].enable(); + } + }); + } } public get changed(): boolean { @@ -44,79 +62,23 @@ export class AddEditUserDialogComponent { return false; } - private async _updateProfile() { - const profileKeys = ['firstName', 'lastName']; - let profileChanged = false; - for (const key of profileKeys) { - if (this.userForm.get(key).value !== this.user[key]) { - profileChanged = true; - } - } - if (!profileChanged) { - return; - } - await this._userControllerService.updateProfile(this.userForm.getRawValue()).toPromise(); - } - - private async _updateRoles() { - const newAdminValue = this.userForm.get('admin').value; - if (newAdminValue === this._userService.isAdmin(this.user)) { - return; - } - let roles = [...this.user.roles]; - if (newAdminValue) { - roles.push('RED_ADMIN'); - } else { - roles = roles.filter((role) => role !== 'RED_ADMIN'); - } - await this._userControllerService.addRoleToUsers(roles, this.user.userId).toPromise(); - } - - private async _update() { - await this._updateProfile(); - await this._updateRoles(); - } - - private async _create() { - const roles = ['RED_USER']; - if (this.userForm.get('admin').value) { - roles.push('RED_ADMIN'); - } - await this._userControllerService.createUser({ ...this.userForm.getRawValue(), roles: [] }, 'body').toPromise(); - } - public async save() { - if (this.user) { - await this._update(); - } else { - await this._create(); - } - this.dialogRef.close(true); + this.dialogRef.close({ + action: this.user ? 'UPDATE' : 'CREATE', + user: { ...this.userForm.getRawValue(), roles: this.activeRoles } + }); } public async delete() { this.dialogRef.close('DELETE'); } - public changeRoleValue(role: string, { checked }: MatCheckboxChange) { - const roles = [...this.activeRoles]; - if (checked) { - roles.push(role); - } else { - roles.splice(roles.indexOf(role), 1); - } - this.userForm.patchValue({ roles }); - } - public get activeRoles(): string[] { - return this.userForm.get('roles').value; - } - - public getChecked(role: string) { - return this.activeRoles.indexOf(role) !== -1; - } - - public getDisabled(role: string) { - return this.getChecked(role) && role === 'RED_USER' && this.getChecked('RED_ADMIN'); + return this.ROLES.reduce((acc, role) => { + if (this.userForm.get(role).value) { + acc.push(role); + } + return acc; + }, []); } } diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html index 0d4366f84..cc1570ce2 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.html @@ -12,7 +12,7 @@
- {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { count: 20 } }} + {{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { projectsCount: projectsCount } }}
diff --git a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts index 8b288077f..797b28590 100644 --- a/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts +++ b/apps/red-ui/src/app/modules/admin/dialogs/confirm-delete-user-dialog/confirm-delete-user-dialog.component.ts @@ -1,6 +1,7 @@ import { Component, Inject, OnInit } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; -import { User, UserControllerService } from '@redaction/red-ui-http'; +import { User } from '@redaction/red-ui-http'; +import { AppStateService } from '../../../../state/app-state.service'; @Component({ selector: 'redaction-confirm-delete-user-dialog', @@ -10,18 +11,20 @@ import { User, UserControllerService } from '@redaction/red-ui-http'; export class ConfirmDeleteUserDialogComponent implements OnInit { public checkboxes = [{ value: false }, { value: false }]; public showToast = false; + public projectsCount: number; constructor( @Inject(MAT_DIALOG_DATA) public user: User, - private readonly _userControllerService: UserControllerService, + private readonly _appStateService: AppStateService, public dialogRef: MatDialogRef - ) {} + ) { + this.projectsCount = this._appStateService.allProjects.filter((pw) => pw.memberIds.indexOf(user.userId) !== -1).length; + } ngOnInit(): void {} async deleteUser() { if (this.valid) { - await this._userControllerService.deleteUser(this.user.userId).toPromise(); this.dialogRef.close(true); } else { this.showToast = true; diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html index 13a13a447..e96a3431e 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.html @@ -23,7 +23,7 @@
-
+
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }} @@ -78,7 +78,9 @@
-
+
+ +
diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss index dc3ae3d31..89360c074 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.scss @@ -28,6 +28,15 @@ display: flex; width: 353px; min-width: 353px; + padding: 16px 16px 16px 24px; + + &.has-scrollbar:hover { + padding-right: 5px; + } + + redaction-users-stats { + width: 100%; + } } .page-header .actions { diff --git a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts index dd09f094d..d57f90fa6 100644 --- a/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts +++ b/apps/red-ui/src/app/modules/admin/screens/user-listing/user-listing-screen.component.ts @@ -6,6 +6,8 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { debounce } from '../../../../utils/debounce'; import { AdminDialogService } from '../../services/admin-dialog-service.service'; import { TranslateService } from '@ngx-translate/core'; +import { DoughnutChartConfig } from '../../../shared/components/simple-doughnut-chart/simple-doughnut-chart.component'; +import { TranslateChartService } from '../../../../services/translate-chart.service'; @Component({ selector: 'redaction-user-listing-screen', @@ -14,7 +16,8 @@ import { TranslateService } from '@ngx-translate/core'; }) export class UserListingScreenComponent implements OnInit { public viewReady = false; - + public collapsedDetails = false; + public chartData: DoughnutChartConfig[]; public users: User[]; public displayedUsers: User[] = []; public searchForm: FormGroup; @@ -25,7 +28,8 @@ export class UserListingScreenComponent implements OnInit { private readonly _formBuilder: FormBuilder, private readonly _translateService: TranslateService, private readonly _adminDialogService: AdminDialogService, - private readonly _userControllerService: UserControllerService + private readonly _userControllerService: UserControllerService, + private readonly _translateChartService: TranslateChartService ) { this.searchForm = this._formBuilder.group({ query: [''] @@ -46,10 +50,16 @@ export class UserListingScreenComponent implements OnInit { public openAddEditUserDialog($event: MouseEvent, user?: User) { $event.stopPropagation(); - this._adminDialogService.openAddEditUserDialog(user, async (action) => { - if (action === 'DELETE') { + this._adminDialogService.openAddEditUserDialog(user, async (result) => { + if (result === 'DELETE') { this.openDeleteUserDialog(user); } else { + this.viewReady = false; + if (result.action === 'CREATE') { + await this._userControllerService.createUser(result.user).toPromise(); + } else if (result.action === 'UPDATE') { + await this._userControllerService.updateProfile(result.user).toPromise(); + } await this._loadData(); } }); @@ -58,6 +68,8 @@ export class UserListingScreenComponent implements OnInit { public openDeleteUserDialog(user: User, $event?: MouseEvent) { $event?.stopPropagation(); this._adminDialogService.openConfirmDeleteUserDialog(user, async () => { + this.viewReady = false; + await this._userControllerService.deleteUser(user.userId).toPromise(); await this._loadData(); }); } @@ -66,9 +78,47 @@ export class UserListingScreenComponent implements OnInit { this.viewReady = false; this.users = (await this._userControllerService.getAllUsers({}).toPromise()).users; this._executeSearch(); + this._computeStats(); this.viewReady = true; } + private _computeStats() { + this.chartData = this._translateChartService.translateRoles( + [ + { + value: this.users.filter((user) => !this.userService.isActive(user)).length, + color: 'INACTIVE', + label: 'INACTIVE' + }, + { + value: this.users.filter((user) => user.roles.length === 1 && user.roles[0] === 'RED_USER').length, + color: 'REGULAR', + label: 'REGULAR' + }, + { + value: this.users.filter((user) => this.userService.isManager(user) && !this.userService.isAdmin(user)).length, + color: 'MANAGER', + label: 'RED_MANAGER' + }, + { + value: this.users.filter((user) => this.userService.isManager(user) && this.userService.isAdmin(user)).length, + color: 'MANAGER_ADMIN', + label: 'MANAGER_ADMIN' + }, + { + value: this.users.filter((user) => this.userService.isUserAdmin(user) && !this.userService.isAdmin(user)).length, + color: 'USER_ADMIN', + label: 'RED_USER_ADMIN' + }, + { + value: this.users.filter((user) => this.userService.isAdmin(user) && !this.userService.isManager(user)).length, + color: 'ADMIN', + label: 'RED_ADMIN' + } + ].filter((type) => type.value > 0) + ); + } + public getDisplayRoles(user: User) { return user.roles.map((role) => this._translateService.instant('roles.' + role)).join(', ') || this._translateService.instant('roles.NO_ROLE'); } @@ -79,4 +129,8 @@ export class UserListingScreenComponent implements OnInit { await this._userControllerService.addRoleToUsers(user.roles, user.userId).toPromise(); await this._loadData(); } + + public toggleCollapsedDetails() { + this.collapsedDetails = !this.collapsedDetails; + } } diff --git a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html index 6c27d7bc5..f92abf6a9 100644 --- a/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html +++ b/apps/red-ui/src/app/modules/projects/components/project-details/project-details.component.html @@ -1,5 +1,5 @@ -
+
*:not(:last-child) { - margin-right: 20px; + margin-right: 16px; } } @@ -27,7 +27,6 @@ .text-container { position: absolute; - top: 0; display: flex; flex-direction: column; justify-content: center; diff --git a/apps/red-ui/src/app/services/translate-chart.service.ts b/apps/red-ui/src/app/services/translate-chart.service.ts index 7e2b0f502..d1ea1582f 100644 --- a/apps/red-ui/src/app/services/translate-chart.service.ts +++ b/apps/red-ui/src/app/services/translate-chart.service.ts @@ -11,4 +11,8 @@ export class TranslateChartService { public translateStatus(config: DoughnutChartConfig[]): DoughnutChartConfig[] { return config.map((val) => ({ ...val, label: this._translateService.instant(val.label) })); } + + public translateRoles(config: DoughnutChartConfig[]): DoughnutChartConfig[] { + return config.map((val) => ({ ...val, label: this._translateService.instant(`roles.${val.label}`).toLowerCase() })); + } } diff --git a/apps/red-ui/src/app/services/user.service.ts b/apps/red-ui/src/app/services/user.service.ts index fa07b9c20..bb174b215 100644 --- a/apps/red-ui/src/app/services/user.service.ts +++ b/apps/red-ui/src/app/services/user.service.ts @@ -114,6 +114,13 @@ export class UserService { return user.roles?.indexOf('RED_USER') >= 0; } + isUserAdmin(user?: User): boolean { + if (!user) { + user = this.user; + } + return user.roles?.indexOf('RED_USER_ADMIN') >= 0; + } + isAdmin(user?: User): boolean { if (!user) { user = this.user; diff --git a/apps/red-ui/src/assets/i18n/en.json b/apps/red-ui/src/assets/i18n/en.json index 4a312b587..b01f4e634 100644 --- a/apps/red-ui/src/assets/i18n/en.json +++ b/apps/red-ui/src/assets/i18n/en.json @@ -761,8 +761,8 @@ "confirm-delete-user": { "title": "Delete User from Workspace", "warning": "Warning: this cannot be undone!", - "checkbox-1": "{{count}} projects will be impacted", - "checkbox-2": "{{count}} documents waiting review will be impacted", + "checkbox-1": "{{projectsCount}} projects will be impacted", + "checkbox-2": "All documents waiting review from the user will be impacted", "delete": "Delete User", "cancel": "Keep User", "toast-error": "Please confirm that you understand the ramifications of your action!" @@ -807,6 +807,14 @@ "cancel": "Cancel" } }, + "user-stats": { + "title": "Users", + "chart": { + "users": "Users in Workspace" + }, + "expand": "Show Details", + "collapse": "Hide Details" + }, "rules-screen": { "error": { "generic": "Something went wrong... Rules update failed!" @@ -1057,6 +1065,9 @@ "RED_MANAGER": "Manager", "RED_USER_ADMIN": "Users Admin", "RED_ADMIN": "Application Admin", - "NO_ROLE": "No role defined" + "NO_ROLE": "No role defined", + "INACTIVE": "Inactive", + "MANAGER_ADMIN": "Manager & Admin", + "REGULAR": "Regular" } } diff --git a/apps/red-ui/src/assets/styles/red-components.scss b/apps/red-ui/src/assets/styles/red-components.scss index e0cef767b..ff2abf5c5 100644 --- a/apps/red-ui/src/assets/styles/red-components.scss +++ b/apps/red-ui/src/assets/styles/red-components.scss @@ -111,7 +111,8 @@ background-color: $grey-3; } -.UNDER_REVIEW { +.UNDER_REVIEW, +.REGULAR { stroke: $yellow-1; background-color: $yellow-1; } @@ -121,7 +122,8 @@ background-color: $blue-4; } -.APPROVED { +.APPROVED, +.ADMIN { stroke: $blue-3; background-color: $blue-3; } @@ -131,7 +133,8 @@ background-color: $grey-1; } -.OCR_PROCESSING { +.OCR_PROCESSING, +.USER_ADMIN { stroke: $green-2; background-color: $green-2; } @@ -161,6 +164,17 @@ background-color: rgba(#0389ec, 0.1); } +.INACTIVE { + stroke: $grey-5; + background-color: $grey-5; +} + +.MANAGER, +.MANAGER_ADMIN { + stroke: $red-1; + background-color: $red-1; +} + .select-oval { width: 20px; height: 20px;