User management updates

This commit is contained in:
Adina Țeudan 2021-04-07 00:49:43 +03:00
parent fe57bba05e
commit 7289c8675e
18 changed files with 221 additions and 100 deletions

View File

@ -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
];

View File

@ -0,0 +1,25 @@
<div class="collapsed-wrapper">
<redaction-circle-button (action)="toggleCollapse.emit()" icon="red:expand" tooltip="user-stats.expand" tooltipPosition="before"></redaction-circle-button>
<div class="all-caps-label" translate="user-stats.title"></div>
</div>
<div class="header-wrapper mt-8">
<div class="heading-xl flex-1" translate="user-stats.title"></div>
<redaction-circle-button
(action)="toggleCollapse.emit()"
icon="red:collapse"
tooltip="user-stats.collapse"
tooltipPosition="before"
></redaction-circle-button>
</div>
<div class="mt-44">
<redaction-simple-doughnut-chart
[config]="chartData"
[strokeWidth]="15"
[radius]="63"
[subtitle]="'user-stats.chart.users'"
totalType="sum"
direction="row"
></redaction-simple-doughnut-chart>
</div>

View File

@ -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;
}

View File

@ -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 {}
}

View File

@ -28,13 +28,7 @@
<div class="red-input-group">
<label translate="add-edit-user.form.role"></label>
<div class="roles-wrapper">
<mat-checkbox
*ngFor="let role of ROLES"
color="primary"
[checked]="getChecked(role)"
[disabled]="getDisabled(role)"
(change)="changeRoleValue(role, $event)"
>
<mat-checkbox [formControlName]="role" *ngFor="let role of ROLES" color="primary">
{{ 'roles.' + role | translate }}
</mat-checkbox>
</div>

View File

@ -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<AddEditUserDialogComponent>,
@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;
}, []);
}
}

View File

@ -12,7 +12,7 @@
<div class="heading" translate="confirm-delete-user.warning"></div>
<mat-checkbox *ngFor="let checkbox of checkboxes; let idx = index" [(ngModel)]="checkbox.value" color="primary">
{{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { count: 20 } }}
{{ 'confirm-delete-user.checkbox-' + (idx + 1) | translate: { projectsCount: projectsCount } }}
</mat-checkbox>
</div>

View File

@ -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<ConfirmDeleteUserDialogComponent>
) {}
) {
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;

View File

@ -23,7 +23,7 @@
</div>
<div class="red-content-inner">
<div class="left-container">
<div class="left-container" [class.extended]="collapsedDetails">
<div class="header-item">
<span class="all-caps-label">
{{ 'user-listing.table-header.title' | translate: { length: displayedUsers.length } }}
@ -78,7 +78,9 @@
</cdk-virtual-scroll-viewport>
</div>
<div class="right-container"></div>
<div class="right-container" redactionHasScrollbar [class.collapsed]="collapsedDetails">
<redaction-users-stats (toggleCollapse)="toggleCollapsedDetails()" [chartData]="chartData"></redaction-users-stats>
</div>
</div>
</section>

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -1,5 +1,5 @@
<ng-container *ngIf="appStateService.activeProject">
<div class="collapsed-wrapper mt-8">
<div class="collapsed-wrapper">
<redaction-circle-button
(action)="toggleCollapse.emit()"
icon="red:expand"

View File

@ -11,7 +11,7 @@
redaction-circle-button {
position: absolute;
top: -5px;
top: -8px;
left: 290px;
}
}

View File

@ -11,7 +11,7 @@
&:not(.column) {
> *: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;

View File

@ -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() }));
}
}

View File

@ -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;

View File

@ -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"
}
}

View File

@ -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;