Pull request #54: User management

Merge in RED/ui from user-management to master

* commit 'db56af4758786161396ec05eb2b3496a83788df8':
  Routing, breadcrumbs, users page
This commit is contained in:
Timo Bejan 2020-12-07 14:17:09 +01:00
commit 5915635e4e
15 changed files with 297 additions and 114 deletions

View File

@ -89,11 +89,112 @@ import { DictionaryOverviewScreenComponent } from './screens/admin/dictionary-ov
import { ColorPickerModule } from 'ngx-color-picker';
import { AceEditorModule } from 'ng2-ace-editor';
import { TeamMembersComponent } from './components/team-members/team-members.component';
import { AdminBreadcrumbsComponent } from './components/admin-page-header/admin-breadcrumbs.component';
import { UserListingScreenComponent } from './screens/admin/users/user-listing-screen.component';
export function HttpLoaderFactory(httpClient: HttpClient) {
return new TranslateHttpLoader(httpClient, '/assets/i18n/', '.json');
}
const routes = [
{
path: '',
redirectTo: 'ui/projects',
pathMatch: 'full'
},
{
path: 'auth-error',
component: AuthErrorComponent,
canActivate: [AuthGuard]
},
{
path: 'info',
component: AppInfoComponent
},
{
path: 'ui',
component: BaseScreenComponent,
children: [
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'admin',
children: [
{ path: '', redirectTo: 'dictionaries', pathMatch: 'full' },
{
path: 'dictionaries',
component: DictionaryListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'dictionaries/:type',
component: DictionaryOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'users',
component: UserListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
}
]
}
];
const matImports = [
MatDialogModule,
MatNativeDateModule,
MatToolbarModule,
MatButtonModule,
MatSlideToggleModule,
MatMenuModule,
MatIconModule,
MatTooltipModule,
MatSnackBarModule,
MatTabsModule,
MatButtonToggleModule,
MatFormFieldModule,
MatProgressSpinnerModule,
MatCheckboxModule,
MatListModule,
MatDatepickerModule,
MatInputModule,
MatSelectModule,
MatSidenavModule
];
@NgModule({
declarations: [
AppComponent,
@ -116,7 +217,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
AuthErrorComponent,
HumanizePipe,
CommentsComponent,
HumanizePipe,
ToastComponent,
FilterComponent,
AppInfoComponent,
@ -144,7 +244,9 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
SyncWidthDirective,
AddEditDictionaryDialogComponent,
DictionaryOverviewScreenComponent,
TeamMembersComponent
TeamMembersComponent,
AdminBreadcrumbsComponent,
UserListingScreenComponent
],
imports: [
BrowserModule,
@ -155,8 +257,6 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
AuthModule,
IconsModule,
ApiModule,
MatDialogModule,
MatNativeDateModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@ -164,93 +264,16 @@ export function HttpLoaderFactory(httpClient: HttpClient) {
deps: [HttpClient]
}
}),
RouterModule.forRoot([
{
path: '',
redirectTo: 'ui/projects',
pathMatch: 'full'
},
{
path: 'auth-error',
component: AuthErrorComponent,
canActivate: [AuthGuard]
},
{
path: 'info',
component: AppInfoComponent
},
{
path: 'ui',
component: BaseScreenComponent,
children: [
{
path: 'projects',
component: ProjectListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId',
component: ProjectOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'projects/:projectId/file/:fileId',
component: FilePreviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'admin-dictionaries',
component: DictionaryListingScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
},
{
path: 'dictionary-overview/:type',
component: DictionaryOverviewScreenComponent,
canActivate: [CompositeRouteGuard],
data: {
routeGuards: [AuthGuard, RedRoleGuard, AppStateGuard]
}
}
]
}
]),
RouterModule.forRoot(routes),
NgpSortModule,
MatToolbarModule,
MatButtonModule,
MatSlideToggleModule,
MatMenuModule,
MatIconModule,
MatTooltipModule,
MatSnackBarModule,
MatTabsModule,
MatButtonToggleModule,
MatFormFieldModule,
...matImports,
ToastrModule.forRoot({
closeButton: true,
enableHtml: true,
toastComponent: ToastComponent
}),
MatSelectModule,
MatSidenavModule,
FileUploadModule,
ServiceWorkerModule.register('ngsw-worker.js', { enabled: environment.production }),
MatProgressSpinnerModule,
MatCheckboxModule,
MatListModule,
MatDatepickerModule,
MatInputModule,
ColorPickerModule,
AceEditorModule
],

View File

@ -1,12 +1,9 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { KeycloakService } from 'keycloak-angular';
import { UserService } from '../user/user.service';
import { AppLoadStateService } from '../utils/app-load-state.service';
import { Observable } from 'rxjs';
class UrlTree {}
@Injectable({
providedIn: 'root'
})
@ -22,7 +19,7 @@ export class RedRoleGuard implements CanActivate {
obs.complete();
} else {
if (!this._userService.isUser() && state.url.startsWith('/ui/projects')) {
this._router.navigate(['/ui/admin-dictionaries']);
this._router.navigate(['/ui/admin']);
obs.next(false);
obs.complete();
}

View File

@ -0,0 +1,26 @@
<div class="menu flex-2 visible-lg breadcrumbs-container">
<a
class="breadcrumb"
routerLink="/ui/admin/dictionaries"
translate="dictionaries"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
*ngIf="screen === 'dictionaries' || root"
></a>
<a
class="ml-32 breadcrumb"
[routerLink]="'/ui/admin/users'"
[routerLinkActiveOptions]="{ exact: true }"
routerLinkActive="active"
translate="user-management"
*ngIf="screen === 'users' || root"
></a>
<ng-container *ngIf="dictionary">
<mat-icon svgIcon="red:arrow-right"></mat-icon>
<a class="breadcrumb" [routerLink]="'/ui/admin/dictionaries/' + dictionary.type" routerLinkActive="active">
{{ dictionary.type | humanize }}
</a>
</ng-container>
</div>

View File

@ -0,0 +1,3 @@
.ml-32 {
margin-left: 32px;
}

View File

@ -0,0 +1,28 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { TypeValue } from '@redaction/red-ui-http';
import { AppStateService } from '../../state/app-state.service';
@Component({
selector: 'redaction-admin-breadcrumbs',
templateUrl: './admin-breadcrumbs.component.html',
styleUrls: ['./admin-breadcrumbs.component.scss']
})
export class AdminBreadcrumbsComponent implements OnInit {
public dictionary: TypeValue;
public root: boolean;
public screen: string;
constructor(private readonly _activatedRoute: ActivatedRoute, private _appStateService: AppStateService) {
this._activatedRoute.params.subscribe((params) => {
const url = this._activatedRoute.snapshot.url;
this.root = url.length === 1;
this.screen = url[0].path;
if (this.screen === 'dictionaries' && url.length === 2) {
this.dictionary = this._appStateService.dictionaryData[params.type];
}
});
}
ngOnInit(): void {}
}

View File

@ -1,9 +1,6 @@
<section>
<div class="page-header">
<div class="menu flex-2 visible-lg breadcrumbs-container">
<a class="breadcrumb" routerLink="/ui/admin-dictionaries" translate="dictionaries"></a>
<div>&nbsp;</div>
</div>
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
<div class="actions">
<redaction-icon-button
@ -60,7 +57,7 @@
[withSort]="true"
></redaction-table-col-name>
<redaction-table-col-name label="dictionary-listing.table-col-names.hint-redaction" class="flex-center"></redaction-table-col-name>
<div class="placeholder-bottom-border scrollbar-placeholder"></div>
<div class="placeholder-bottom-border"></div>
<div class="placeholder-bottom-border scrollbar-placeholder"></div>
</div>
@ -69,7 +66,7 @@
<div
class="table-item pointer"
*ngFor="let dict of dictionaries | sortBy: sortingOption.order:sortingOption.column"
[routerLink]="['/ui/dictionary-overview/' + dict.type]"
[routerLink]="['/ui/admin/dictionaries/' + dict.type]"
>
<div class="pr-0" (click)="toggleDictSelected($event, dict)">
<div *ngIf="!isDictSelected(dict)" class="select-oval"></div>

View File

@ -1,12 +1,6 @@
<section>
<div class="page-header">
<div class="menu flex-2 visible-lg breadcrumbs-container">
<a class="breadcrumb" routerLink="/ui/admin-dictionaries" translate="dictionaries"></a>
<mat-icon svgIcon="red:arrow-right"></mat-icon>
<a class="breadcrumb" [routerLink]="'/ui/dictionary-overview/' + dictionary.type">
{{ dictionary.type | humanize }}
</a>
</div>
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
<div class="actions">
<redaction-circle-button
@ -29,7 +23,7 @@
<redaction-circle-button
class="ml-6"
[routerLink]="['/ui/admin-dictionaries/']"
[routerLink]="['/ui/admin/dictionaries/']"
tooltip="common.close"
tooltipPosition="before"
icon="red:close"

View File

@ -55,7 +55,7 @@ export class DictionaryOverviewScreenComponent {
this._activatedRoute.params.subscribe((params) => {
this.dictionary = this._appStateService.dictionaryData[params.type];
if (!this.dictionary) {
this._router.navigate(['/ui/admin-dictionaries']);
this._router.navigate(['/ui/admin/dictionaries']);
} else {
this._initialize();
}
@ -87,7 +87,7 @@ export class DictionaryOverviewScreenComponent {
openDeleteDictionaryDialog($event: any) {
this._dialogService.openDeleteDictionaryDialog($event, this.dictionary, async () => {
await this._appStateService.loadDictionaryData();
this._router.navigate(['/ui/admin-dictionaries']);
this._router.navigate(['/ui/admin/dictionaries']);
});
}

View File

@ -0,0 +1,46 @@
<section>
<div class="page-header">
<redaction-admin-breadcrumbs></redaction-admin-breadcrumbs>
<div class="actions">
<redaction-circle-button
class="ml-6"
*ngIf="permissionsService.isUser()"
[routerLink]="['/ui/projects/']"
tooltip="common.close"
tooltipPosition="before"
icon="red:close"
></redaction-circle-button>
</div>
</div>
<div class="red-content-inner">
<div class="left-container">
<div class="header-item">
<span class="all-caps-label">
{{ 'user-listing.table-header.title' | translate: { length: users.length } }}
</span>
</div>
<div class="table-header" redactionSyncWidth="table-item">
<redaction-table-col-name label="user-listing.table-col-names.name"></redaction-table-col-name>
<redaction-table-col-name label="user-listing.table-col-names.email"></redaction-table-col-name>
<div class="placeholder-bottom-border scrollbar-placeholder"></div>
</div>
<div class="grid-container">
<!-- Table lines -->
<div class="table-item" *ngFor="let user of users">
<div>
<redaction-initials-avatar [userId]="user.userId" [withName]="true" size="large"></redaction-initials-avatar>
</div>
<div>{{ user.email || '-' }}</div>
<div class="scrollbar-placeholder"></div>
</div>
</div>
</div>
<div class="right-container"></div>
</div>
</section>

View File

@ -0,0 +1,23 @@
.left-container {
width: calc(100vw - 353px);
.grid-container {
grid-template-columns: 1fr 1fr 11px;
&:hover {
grid-template-columns: 1fr 1fr;
}
.table-item {
> div {
padding: 0 24px;
}
}
}
}
.right-container {
display: flex;
width: 353px;
min-width: 353px;
}

View File

@ -0,0 +1,19 @@
import { Component, OnInit } from '@angular/core';
import { PermissionsService } from '../../../common/service/permissions.service';
import { UserService } from '../../../user/user.service';
import { User } from '@redaction/red-ui-http';
@Component({
selector: 'redaction-user-listing-screen',
templateUrl: './user-listing-screen.component.html',
styleUrls: ['./user-listing-screen.component.scss']
})
export class UserListingScreenComponent implements OnInit {
public users: User[];
constructor(public readonly permissionsService: PermissionsService, private readonly userService: UserService) {
this.users = this.userService.allUsers;
}
ngOnInit(): void {}
}

View File

@ -20,9 +20,21 @@
</mat-menu>
</div>
<div class="menu flex-2 visible-lg breadcrumbs-container" *ngIf="permissionsService.isUser()">
<a class="breadcrumb" routerLink="/ui/projects" translate="top-bar.navigation-items.projects"></a>
<a
class="breadcrumb"
routerLink="/ui/projects"
translate="top-bar.navigation-items.projects"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
></a>
<mat-icon *ngIf="appStateService.activeProject" svgIcon="red:arrow-right"></mat-icon>
<a *ngIf="appStateService.activeProject" class="breadcrumb" [routerLink]="'/ui/projects/' + appStateService.activeProjectId">
<a
*ngIf="appStateService.activeProject"
class="breadcrumb"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
{{ appStateService.activeProject.project.projectName }}
</a>
<mat-icon svgIcon="red:arrow-right" *ngIf="appStateService.activeFile"></mat-icon>
@ -30,6 +42,8 @@
*ngIf="appStateService.activeFile"
class="breadcrumb"
[routerLink]="'/ui/projects/' + appStateService.activeProjectId + '/file/' + appStateService.activeFile.fileId"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"
>
{{ appStateService.activeFile.filename }}
</a>
@ -47,9 +61,9 @@
<button
*ngIf="permissionsService.isManager()"
(click)="appStateService.reset()"
[routerLink]="'/ui/admin-dictionaries'"
[routerLink]="'/ui/admin'"
mat-menu-item
translate="top-bar.navigation-items.my-account.children.admin-dictionaries"
translate="top-bar.navigation-items.my-account.children.admin"
></button>
<button [matMenuTriggerFor]="language" mat-menu-item translate="top-bar.navigation-items.my-account.children.language.label"></button>
<mat-menu #language="matMenu">

View File

@ -43,19 +43,19 @@ export class UserService {
this._keycloakService.logout(window.location.origin);
}
get userId() {
get userId(): string {
return this._currentUser.id;
}
get allUsers() {
get allUsers(): User[] {
return this._allUsers;
}
get managerUsers() {
get managerUsers(): User[] {
return this._allUsers.filter((u) => u.roles.indexOf('RED_MANAGER') >= 0);
}
get eligibleUsers() {
get eligibleUsers(): User[] {
return this._allUsers.filter((u) => u.roles.indexOf('RED_USER') >= 0 || u.roles.indexOf('RED_MANAGER') >= 0);
}

View File

@ -41,7 +41,7 @@
"projects": "Projects",
"my-account": {
"children": {
"admin-dictionaries": "Manage Dictionaries",
"admin": "Management",
"language": {
"label": "Language",
"english": "English",
@ -535,5 +535,15 @@
"hint-redaction": "Hint/Redaction"
}
},
"dictionaries": "Dictionaries"
"user-listing": {
"table-header": {
"title": "{{length}} users"
},
"table-col-names": {
"name": "Name",
"email": "Email"
}
},
"dictionaries": "Dictionaries",
"user-management": "User Management"
}

View File

@ -14,9 +14,12 @@
white-space: nowrap;
&:last-child {
color: $primary;
@include line-clamp(1);
}
&.active {
color: $primary;
}
}
mat-icon {