Browse Source

Some progress

pull/271/head
Sebastian Stehle 9 years ago
parent
commit
4facb28893
  1. 10
      src/Squidex/app/features/administration/declarations.ts
  2. 87
      src/Squidex/app/features/administration/guards/resolve-user.guard.spec.ts
  3. 24
      src/Squidex/app/features/administration/guards/unset-user.guard.ts
  4. 48
      src/Squidex/app/features/administration/guards/user-must-exist.guard.ts
  5. 24
      src/Squidex/app/features/administration/module.ts
  6. 2
      src/Squidex/app/features/administration/pages/users/user-page.component.html
  7. 72
      src/Squidex/app/features/administration/pages/users/user-page.component.ts
  8. 30
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  9. 81
      src/Squidex/app/features/administration/pages/users/users-page.component.ts
  10. 130
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  11. 88
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  12. 230
      src/Squidex/app/features/administration/services/users.service.spec.ts
  13. 145
      src/Squidex/app/features/administration/services/users.service.ts
  14. 138
      src/Squidex/app/features/administration/state/users.state.ts
  15. 10
      src/Squidex/app/framework/utils/rxjs-extensions-impl.ts
  16. 20
      src/Squidex/app/framework/utils/rxjs-extensions.ts

10
src/Squidex/app/features/administration/declarations.ts

@ -5,8 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
export * from './administration-area.component';
export * from './guards/user-must-exist.guard';
export * from './guards/unset-user.guard';
export * from './pages/event-consumers/event-consumers-page.component'; export * from './pages/event-consumers/event-consumers-page.component';
export * from './pages/users/user-page.component'; export * from './pages/users/user-page.component';
export * from './pages/users/users-page.component'; export * from './pages/users/users-page.component';
export * from './administration-area.component'; export * from './services/event-consumers.service';
export * from './services/users.service';
export * from './state/users.state';

87
src/Squidex/app/features/administration/guards/resolve-user.guard.spec.ts

@ -0,0 +1,87 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
/*
import { IMock, Mock } from 'typemoq';
import { Observable } from 'rxjs';
import { UserManagementService } from 'shared';
import { ResolveUserGuard } from './resolve-user.guard';
import { RouterMockup } from './router-mockup';
describe('ResolveUserGuard', () => {
const route = {
params: {},
parent: {
params: {
userId: 'my-user'
}
}
};
let usersService: IMock<UserManagementService>;
beforeEach(() => {
usersService = Mock.ofType(UserManagementService);
});
it('should throw if route does not contain parameter', () => {
const guard = new ResolveUserGuard(usersService.object, <any>new RouterMockup());
expect(() => guard.resolve(<any>{ params: {} }, <any>{})).toThrow('Route must contain user id.');
});
it('should navigate to 404 page if user is not found', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should navigate to 404 page if user loading fails', (done) => {
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.throw(null!));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBeFalsy();
expect(router.lastNavigation).toEqual(['/404']);
done();
});
});
it('should return user if loading succeeded', (done) => {
const user: any = {};
usersService.setup(x => x.getUser('my-user'))
.returns(() => Observable.of(user));
const router = new RouterMockup();
const guard = new ResolveUserGuard(usersService.object, <any>router);
guard.resolve(<any>route, <any>{})
.subscribe(result => {
expect(result).toBe(user);
done();
});
});
});
*/

24
src/Squidex/app/features/administration/guards/unset-user.guard.ts

@ -0,0 +1,24 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { UsersState } from './../state/users.state';
@Injectable()
export class UnsetUserGuard implements CanActivate {
constructor(
private readonly usersState: UsersState
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.usersState.selectUser(null).map(r => !r);
}
}

48
src/Squidex/app/features/administration/guards/user-must-exist.guard.ts

@ -0,0 +1,48 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { allParams } from 'framework';
import { UsersState } from './../state/users.state';
@Injectable()
export class UserMustExistGuard implements CanActivate {
constructor(
private readonly usersState: UsersState,
private readonly router: Router
) {
}
public canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const params = allParams(route);
const userId = params['userId'];
if (!userId) {
throw 'Route must contain user id.';
}
const result =
this.usersState.selectUser(userId).map(u => !!u)
.do(dto => {
if (!dto) {
this.router.navigate(['/404']);
}
})
.catch(error => {
this.router.navigate(['/404']);
return Observable.of(false);
});
return result;
}
}

24
src/Squidex/app/features/administration/module.ts

@ -9,16 +9,19 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
import { import {
ResolveUserGuard, SqxFrameworkModule
SqxFrameworkModule,
SqxSharedModule
} from 'shared'; } from 'shared';
import { import {
AdministrationAreaComponent, AdministrationAreaComponent,
EventConsumersPageComponent, EventConsumersPageComponent,
EventConsumersService,
UnsetUserGuard,
UserPageComponent, UserPageComponent,
UsersPageComponent UserMustExistGuard,
UsersPageComponent,
UsersService,
UsersState
} from './declarations'; } from './declarations';
const routes: Routes = [ const routes: Routes = [
@ -39,14 +42,13 @@ const routes: Routes = [
children: [ children: [
{ {
path: 'new', path: 'new',
component: UserPageComponent component: UserPageComponent,
canActivate: [UnsetUserGuard]
}, },
{ {
path: ':userId', path: ':userId',
component: UserPageComponent, component: UserPageComponent,
resolve: { canActivate: [UserMustExistGuard]
user: ResolveUserGuard
}
} }
] ]
} }
@ -59,7 +61,6 @@ const routes: Routes = [
@NgModule({ @NgModule({
imports: [ imports: [
SqxFrameworkModule, SqxFrameworkModule,
SqxSharedModule,
RouterModule.forChild(routes) RouterModule.forChild(routes)
], ],
declarations: [ declarations: [
@ -67,6 +68,11 @@ const routes: Routes = [
EventConsumersPageComponent, EventConsumersPageComponent,
UserPageComponent, UserPageComponent,
UsersPageComponent UsersPageComponent
],
providers: [
EventConsumersService,
UsersService,
UsersState
] ]
}) })
export class SqxFeatureAdministrationModule { } export class SqxFeatureAdministrationModule { }

2
src/Squidex/app/features/administration/pages/users/user-page.component.html

@ -50,7 +50,7 @@
<input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" /> <input type="text" class="form-control" id="displayName" maxlength="100" formControlName="displayName" autocomplete="false" />
</div> </div>
<div class="form-group form-group-password" [class.hidden]="user.id === ctx.userId"> <div class="form-group form-group-password" [class.hidden]="isCurrentUser">
<div class="form-group"> <div class="form-group">
<label for="password">Password</label> <label for="password">Password</label>

72
src/Squidex/app/features/administration/pages/users/user-page.component.ts

@ -5,18 +5,17 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms'; import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Router } from '@angular/router'; import { Subscription } from 'rxjs';
import { import {
AppContext, AppContext,
UserDto, UserDto,
UserManagementService,
ValidatorsEx ValidatorsEx
} from 'shared'; } from 'shared';
import { UserCreated, UserUpdated } from './../messages'; import { UsersState } from './../../state/users.state';
@Component({ @Component({
selector: 'sqx-user-page', selector: 'sqx-user-page',
@ -26,8 +25,8 @@ import { UserCreated, UserUpdated } from './../messages';
AppContext AppContext
] ]
}) })
export class UserPageComponent implements OnInit { export class UserPageComponent implements OnDestroy, OnInit {
public user: UserDto; private selectedUserSubscription: Subscription;
public userFormSubmitted = false; public userFormSubmitted = false;
public userForm: FormGroup; public userForm: FormGroup;
@ -38,18 +37,17 @@ export class UserPageComponent implements OnInit {
constructor(public readonly ctx: AppContext, constructor(public readonly ctx: AppContext,
private readonly formBuilder: FormBuilder, private readonly formBuilder: FormBuilder,
private readonly router: Router, private readonly state: UsersState
private readonly userManagementService: UserManagementService
) { ) {
} }
public ngOnInit() { public ngOnDestroy() {
this.ctx.route.data.map(d => d.user) this.selectedUserSubscription.unsubscribe();
.subscribe((user: UserDto) => { }
this.user = user;
this.setupAndPopulateForm(); public ngOnInit() {
}); this.selectedUserSubscription =
this.state.selectedUser.subscribe(user => this.setupAndPopulateForm(user!));
} }
public save() { public save() {
@ -61,34 +59,19 @@ export class UserPageComponent implements OnInit {
const requestDto = this.userForm.value; const requestDto = this.userForm.value;
if (this.isNewMode) { if (this.isNewMode) {
this.userManagementService.postUser(requestDto) this.state.createUser(requestDto)
.subscribe(created => { .subscribe(() => {
this.user =
new UserDto(
created.id,
requestDto.email,
requestDto.displayName,
created.pictureUrl!,
false);
this.ctx.notifyInfo('User created successfully.'); this.ctx.notifyInfo('User created successfully.');
this.emitUserCreated(this.user); this.resetUserForm();
this.back();
}, error => { }, error => {
this.resetUserForm(error.displayMessage); this.resetUserForm(error.displayMessage);
}); });
} else { } else {
this.userManagementService.putUser(this.user.id, requestDto) this.state.updateUser(requestDto)
.subscribe(() => { .subscribe(() => {
this.user =
this.user.update(
requestDto.email,
requestDto.displayMessage);
this.ctx.notifyInfo('User saved successfully.'); this.ctx.notifyInfo('User saved successfully.');
this.emitUserUpdated(this.user);
this.resetUserForm(); this.resetUserForm();
}, error => { }, error => {
this.resetUserForm(error.displayMessage); this.resetUserForm(error.displayMessage);
@ -97,31 +80,21 @@ export class UserPageComponent implements OnInit {
} }
} }
private back() { private setupAndPopulateForm(user: UserDto | null) {
this.router.navigate(['../'], { relativeTo: this.ctx.route, replaceUrl: true }); const userData: any = user || {};
}
private emitUserCreated(user: UserDto) {
this.ctx.bus.emit(new UserCreated(user));
}
private emitUserUpdated(user: UserDto) {
this.ctx.bus.emit(new UserUpdated(user));
}
private setupAndPopulateForm() { this.isNewMode = !user;
const input = this.user || {}; this.isCurrentUser = user !== null && user.id === this.ctx.userId;
this.isNewMode = !this.user;
this.userForm = this.userForm =
this.formBuilder.group({ this.formBuilder.group({
email: [input['email'], email: [userData.email,
[ [
Validators.email, Validators.email,
Validators.required, Validators.required,
Validators.maxLength(100) Validators.maxLength(100)
]], ]],
displayName: [input['displayName'], displayName: [userData.displayName,
[ [
Validators.required, Validators.required,
Validators.maxLength(100) Validators.maxLength(100)
@ -136,7 +109,6 @@ export class UserPageComponent implements OnInit {
]] ]]
}); });
this.isCurrentUser = this.user && this.user.id === this.ctx.userId;
this.resetUserForm(); this.resetUserForm();
} }

30
src/Squidex/app/features/administration/pages/users/users-page.component.html

@ -10,13 +10,13 @@
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+g" (trigger)="buttonNew.click()"></sqx-shortcut> <sqx-shortcut keys="ctrl+shift+n" (trigger)="buttonNew.click()"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()"> <form class="form-inline" (ngSubmit)="search()">
<input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" /> <input class="form-control" #inputFind [formControl]="usersFilter" placeholder="Search for user" />
</form> </form>
<button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + M)"> <button class="btn btn-success" #buttonNew routerLink="new" title="New User (CTRL + N)">
<i class="icon-plus"></i> New <i class="icon-plus"></i> New
</button> </button>
</div> </div>
@ -56,7 +56,7 @@
<div sqxIgnoreScrollbar> <div sqxIgnoreScrollbar>
<table class="table table-items table-fixed"> <table class="table table-items table-fixed">
<tbody> <tbody>
<ng-template ngFor let-user [ngForOf]="usersItems"> <ng-template ngFor let-user [ngForOf]="state.userItems | async" [ngForTrackBy]="state.trackBy">
<tr [routerLink]="user.id" routerLinkActive="active"> <tr [routerLink]="user.id" routerLinkActive="active">
<td class="cell-user"> <td class="cell-user">
<img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" /> <img class="user-picture" [attr.title]="user.name" [attr.src]="user | sqxUserDtoPicture" />
@ -88,18 +88,20 @@
</div> </div>
</div> </div>
<div class="grid-footer clearfix" *ngIf="usersPager.numberOfItems > 0"> <ng-container *ngIf="state.usersPager | async as pager">
<div class="float-right pagination"> <div class="grid-footer clearfix" *ngIf="pager.numberOfItems > 0">
<span class="pagination-text">{{usersPager.itemFirst}}-{{usersPager.itemLast}} of {{usersPager.numberOfItems}}</span> <div class="float-right pagination">
<span class="pagination-text">{{pager.itemFirst}}-{{pager.itemLast}} of {{pager.numberOfItems}}</span>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoPrev" (click)="goPrev()">
<i class="icon-angle-left"></i> <button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoPrev" (click)="state.goPrev()">
</button> <i class="icon-angle-left"></i>
<button class="btn btn-link btn-secondary pagination-button" [disabled]="!usersPager.canGoNext" (click)="goNext()"> </button>
<i class="icon-angle-right"></i> <button class="btn btn-link btn-secondary pagination-button" [disabled]="!pager.canGoNext" (click)="state.goNext()">
</button> <i class="icon-angle-right"></i>
</button>
</div>
</div> </div>
</div> </ng-container>
</div> </div>
</div> </div>
</sqx-panel> </sqx-panel>

81
src/Squidex/app/features/administration/pages/users/users-page.component.ts

@ -5,19 +5,12 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/ */
import { Component, OnDestroy, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Subscription } from 'rxjs';
import { import { AppContext, UserDto } from 'shared';
AppContext,
ImmutableArray,
Pager,
UserDto,
UserManagementService
} from 'shared';
import { UserCreated, UserUpdated } from './../messages'; import { UsersState } from './../../state/users.state';
@Component({ @Component({
selector: 'sqx-users-page', selector: 'sqx-users-page',
@ -27,91 +20,37 @@ import { UserCreated, UserUpdated } from './../messages';
AppContext AppContext
] ]
}) })
export class UsersPageComponent implements OnDestroy, OnInit { export class UsersPageComponent implements OnInit {
private userCreatedSubscription: Subscription;
private userUpdatedSubscription: Subscription;
public usersItems = ImmutableArray.empty<UserDto>();
public usersPager = new Pager(0);
public usersFilter = new FormControl(); public usersFilter = new FormControl();
public usersQuery = '';
constructor(public readonly ctx: AppContext, constructor(public readonly ctx: AppContext,
private readonly userManagementService: UserManagementService public readonly state: UsersState
) { ) {
} }
public ngOnDestroy() {
this.userCreatedSubscription.unsubscribe();
this.userUpdatedSubscription.unsubscribe();
}
public ngOnInit() { public ngOnInit() {
this.userCreatedSubscription =
this.ctx.bus.of(UserCreated)
.subscribe(message => {
this.usersItems = this.usersItems.pushFront(message.user);
this.usersPager = this.usersPager.incrementCount();
});
this.userUpdatedSubscription =
this.ctx.bus.of(UserUpdated)
.subscribe(message => {
this.usersItems = this.usersItems.replaceBy('id', message.user);
});
this.load(); this.load();
} }
public search() { public search() {
this.usersPager = new Pager(0); this.state.search(this.usersFilter.value);
this.usersQuery = this.usersFilter.value;
this.load();
} }
public load(showInfo = false) { public load(showInfo = false) {
this.userManagementService.getUsers(this.usersPager.pageSize, this.usersPager.skip, this.usersQuery) this.state.loadUsers()
.subscribe(dtos => { .subscribe(() => {
this.usersItems = ImmutableArray.of(dtos.items);
this.usersPager = this.usersPager.setCount(dtos.total);
if (showInfo) { if (showInfo) {
this.ctx.notifyInfo('Users reloaded.'); this.ctx.notifyInfo('Users reloaded.');
} }
}, error => {
this.ctx.notifyError(error);
}); });
} }
public lock(user: UserDto) { public lock(user: UserDto) {
this.userManagementService.lockUser(user.id) this.state.lockUser(user.id).subscribe();
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.lock());
}, error => {
this.ctx.notifyError(error);
});
} }
public unlock(user: UserDto) { public unlock(user: UserDto) {
this.userManagementService.unlockUser(user.id) this.state.unlockUser(user.id).subscribe();
.subscribe(() => {
this.usersItems = this.usersItems.replaceBy('id', user.unlock());
}, error => {
this.ctx.notifyError(error);
});
}
public goNext() {
this.usersPager = this.usersPager.goNext();
this.load();
}
public goPrev() {
this.usersPager = this.usersPager.goPrev();
this.load();
} }
} }

130
src/Squidex/app/features/administration/services/event-consumers.service.spec.ts

@ -0,0 +1,130 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig } from 'framework';
import { EventConsumerDto, EventConsumersService } from './event-consumers.service';
describe('EventConsumerDto', () => {
it('should update isStopped property when starting', () => {
const consumer_1 = new EventConsumerDto('consumer', true, false, 'error', 'position');
const consumer_2 = consumer_1.start();
expect(consumer_2.isStopped).toBeFalsy();
});
it('should update isStopped property when starting', () => {
const consumer_1 = new EventConsumerDto('consumer', false, false, 'error', 'position');
const consumer_2 = consumer_1.stop();
expect(consumer_2.isStopped).toBeTruthy();
});
it('should update isResetting property when resetting', () => {
const consumer_1 = new EventConsumerDto('consumer', false, false, 'error', 'position');
const consumer_2 = consumer_1.reset();
expect(consumer_2.isResetting).toBeTruthy();
});
});
describe('EventConsumersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
EventConsumersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get event consumers',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
let eventConsumers: EventConsumerDto[] | null = null;
eventConsumersService.getEventConsumers().subscribe(result => {
eventConsumers = result;
});
const req = httpMock.expectOne('http://service/p/api/event-consumers');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush([
{
name: 'event-consumer1',
position: '13',
isStopped: true,
isResetting: true,
error: 'an error 1'
},
{
name: 'event-consumer2',
position: '29',
isStopped: true,
isResetting: true,
error: 'an error 2'
}
]);
expect(eventConsumers).toEqual([
new EventConsumerDto('event-consumer1', true, true, 'an error 1', '13'),
new EventConsumerDto('event-consumer2', true, true, 'an error 2', '29')
]);
}));
it('should make put request to start event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
eventConsumersService.startEventConsumer('event-consumer1').subscribe();
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/start');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to stop event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
eventConsumersService.stopEventConsumer('event-consumer1').subscribe();
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/stop');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to reset event consumer',
inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => {
eventConsumersService.resetEventConsumer('event-consumer1').subscribe();
const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/reset');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

88
src/Squidex/app/features/administration/services/event-consumers.service.ts

@ -0,0 +1,88 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import { ApiUrlConfig, HTTP } from 'framework';
export class EventConsumerDto {
constructor(
public readonly name: string,
public readonly isStopped: boolean,
public readonly isResetting: boolean,
public readonly error: string,
public readonly position: string
) {
}
public start(): EventConsumerDto {
return new EventConsumerDto(this.name, false, false, this.error, this.position);
}
public stop(): EventConsumerDto {
return new EventConsumerDto(this.name, true, false, this.error, this.position);
}
public reset(): EventConsumerDto {
return new EventConsumerDto(this.name, this.isStopped, true, this.error, this.position);
}
}
@Injectable()
export class EventConsumersService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getEventConsumers(): Observable<EventConsumerDto[]> {
const url = this.apiUrl.buildUrl('/api/event-consumers');
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body;
return items.map(item => {
return new EventConsumerDto(
item.name,
item.isStopped,
item.isResetting,
item.error,
item.position);
});
})
.pretifyError('Failed to load event consumers. Please reload.');
}
public startEventConsumer(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/start`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to start event consumer. Please reload.');
}
public stopEventConsumer(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/stop`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to stop event consumer. Please reload.');
}
public resetEventConsumer(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/reset`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to reset event consumer. Please reload.');
}
}

230
src/Squidex/app/features/administration/services/users.service.spec.ts

@ -0,0 +1,230 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { inject, TestBed } from '@angular/core/testing';
import { ApiUrlConfig } from 'framework';
import {
CreateUserDto,
UpdateUserDto,
UserDto,
UsersService,
UsersDto
} from './users.service';
describe('UserDto', () => {
it('should update email and display name property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true);
const user_2 = user_1.update('qaisar@squidex.io', 'Qaisar');
expect(user_2.email).toEqual('qaisar@squidex.io');
expect(user_2.displayName).toEqual('Qaisar');
});
it('should update isLocked property when locking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', false);
const user_2 = user_1.lock();
expect(user_2.isLocked).toBeTruthy();
});
it('should update isLocked property when unlocking', () => {
const user_1 = new UserDto('1', 'sebastian@squidex.io', 'Sebastian', 'picture', true);
const user_2 = user_1.unlock();
expect(user_2.isLocked).toBeFalsy();
});
});
describe('UsersService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule
],
providers: [
UsersService,
{ provide: ApiUrlConfig, useValue: new ApiUrlConfig('http://service/p/') }
]
});
});
afterEach(inject([HttpTestingController], (httpMock: HttpTestingController) => {
httpMock.verify();
}));
it('should make get request to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30).subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
}
]
});
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
]));
}));
it('should make get request with query to get many users',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let users: UsersDto | null = null;
userManagementService.getUsers(20, 30, 'my-query').subscribe(result => {
users = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management?take=20&skip=30&query=my-query');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
total: 100,
items: [
{
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
},
{
id: '456',
email: 'mail2@domain.com',
displayName: 'User2',
pictureUrl: 'path/to/image2',
isLocked: true
}
]
});
expect(users).toEqual(
new UsersDto(100, [
new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true),
new UserDto('456', 'mail2@domain.com', 'User2', 'path/to/image2', true)
]));
}));
it('should make get request to get single user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
let user: UserDto | null = null;
userManagementService.getUser('123').subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('GET');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({
id: '123',
email: 'mail1@domain.com',
displayName: 'User1',
pictureUrl: 'path/to/image1',
isLocked: true
});
expect(user).toEqual(new UserDto('123', 'mail1@domain.com', 'User1', 'path/to/image1', true));
}));
it('should make post request to create user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new CreateUserDto('mail@squidex.io', 'Squidex User', 'password');
let user: UserDto | null = null;
userManagementService.postUser(dto).subscribe(result => {
user = result;
});
const req = httpMock.expectOne('http://service/p/api/user-management');
expect(req.request.method).toEqual('POST');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({ id: '123', pictureUrl: 'path/to/image1' });
expect(user).toEqual(new UserDto('123', dto.email, dto.displayName, 'path/to/image1', false));
}));
it('should make put request to update user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
const dto = new UpdateUserDto('mail@squidex.io', 'Squidex User', 'password');
userManagementService.putUser('123', dto).subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to lock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.lockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/lock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
it('should make put request to unlock user',
inject([UsersService, HttpTestingController], (userManagementService: UsersService, httpMock: HttpTestingController) => {
userManagementService.unlockUser('123').subscribe();
const req = httpMock.expectOne('http://service/p/api/user-management/123/unlock');
expect(req.request.method).toEqual('PUT');
expect(req.request.headers.get('If-Match')).toBeNull();
req.flush({});
}));
});

145
src/Squidex/app/features/administration/services/users.service.ts

@ -0,0 +1,145 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import 'framework/angular/http-extensions';
import { ApiUrlConfig, HTTP } from 'framework';
export class UsersDto {
constructor(
public readonly total: number,
public readonly items: UserDto[]
) {
}
}
export class UserDto {
constructor(
public readonly id: string,
public readonly email: string,
public readonly displayName: string,
public readonly pictureUrl: string | null,
public readonly isLocked: boolean
) {
}
public update(email: string, displayName: string): UserDto {
return new UserDto(this.id, email, displayName, this.pictureUrl, this.isLocked);
}
public lock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, true);
}
public unlock(): UserDto {
return new UserDto(this.id, this.email, this.displayName, this.pictureUrl, false);
}
}
export class CreateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
export class UpdateUserDto {
constructor(
public readonly email: string,
public readonly displayName: string,
public readonly password: string
) {
}
}
@Injectable()
export class UsersService {
constructor(
private readonly http: HttpClient,
private readonly apiUrl: ApiUrlConfig
) {
}
public getUsers(take: number, skip: number, query?: string): Observable<UsersDto> {
const url = this.apiUrl.buildUrl(`api/user-management?take=${take}&skip=${skip}&query=${query || ''}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
const items: any[] = body.items;
const users = items.map(item => {
return new UserDto(
item.id,
item.email,
item.displayName,
item.pictureUrl,
item.isLocked);
});
return new UsersDto(body.total, users);
})
.pretifyError('Failed to load users. Please reload.');
}
public getUser(id: string): Observable<UserDto> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.getVersioned<any>(this.http, url)
.map(response => {
const body = response.payload.body;
return new UserDto(
body.id,
body.email,
body.displayName,
body.pictureUrl,
body.isLocked);
})
.pretifyError('Failed to load user. Please reload.');
}
public postUser(dto: CreateUserDto): Observable<UserDto> {
const url = this.apiUrl.buildUrl('api/user-management');
return HTTP.postVersioned<any>(this.http, url, dto)
.map(response => {
const body = response.payload.body;
return new UserDto(body.id, dto.email, dto.displayName, body.pictureUrl, false);
})
.pretifyError('Failed to create user. Please reload.');
}
public putUser(id: string, dto: UpdateUserDto): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}`);
return HTTP.putVersioned(this.http, url, dto)
.pretifyError('Failed to update user. Please reload.');
}
public lockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/lock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
public unlockUser(id: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/user-management/${id}/unlock`);
return HTTP.putVersioned(this.http, url, {})
.pretifyError('Failed to load users. Please retry.');
}
}

138
src/Squidex/app/features/administration/state/users.state.ts

@ -0,0 +1,138 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import 'framework/utils/rxjs-extensions';
import {
DialogService,
ErrorDto,
ImmutableArray,
Notification,
Pager
} from 'shared';
import {
CreateUserDto,
UserDto,
UsersService
} from './../services/users.service';
@Injectable()
export class UsersState {
public usersItems = new BehaviorSubject<ImmutableArray<UserDto>>(ImmutableArray.empty());
public usersPager = new BehaviorSubject(new Pager(0));
public usersQuery = new BehaviorSubject('');
public selectedUser = new BehaviorSubject<UserDto | null>(null);
constructor(
private readonly usersService: UsersService,
private readonly dialogs: DialogService
) {
}
public selectUser(id: string | null): Observable<boolean> {
const observable =
!id ?
Observable.of(null) :
Observable.of(this.usersItems.value.find(x => x.id === id))
.switchMap(user => {
if (!user) {
return this.usersService.getUser(id).catch(() => Observable.of(null));
} else {
return Observable.of(user);
}
});
return observable
.do(user => {
this.selectedUser.next(user);
})
.map(u => u !== null);
}
public loadUsers(): Observable<any> {
return this.usersService.getUsers(this.usersPager.value.pageSize, this.usersPager.value.skip, this.usersQuery.value)
.catch(error => this.notifyError(error))
.do(dtos => {
this.usersItems.nextBy(v => ImmutableArray.of(dtos.items));
this.usersPager.nextBy(v => v.setCount(dtos.total));
});
}
public updateUser(request: CreateUserDto): Observable<any> {
const id = this.selectedUser.value!.id;
return this.usersService.putUser(id, request)
.do(() => {
this.dialogs.notify(Notification.info('User saved successsfull'));
this.usersItems.nextBy(v => v.replaceAll(x => x.id === id, u => u.update(request.email, request.displayName)));
});
}
public createUser(request: CreateUserDto): Observable<UserDto> {
return this.usersService.postUser(request)
.catch(error => this.notifyError(error))
.do(user => {
this.usersItems.nextBy(v => v.pushFront(user));
this.usersPager.nextBy(v => v.incrementCount());
});
}
public lockUser(id: string): Observable<any> {
return this.usersService.lockUser(id)
.catch(error => this.notifyError(error))
.do(() => {
this.usersItems.nextBy(v => v.replaceAll(x => x.id === id, u => u.lock()));
});
}
public unlockUser(id: string): Observable<any> {
return this.usersService.lockUser(id)
.catch(error => this.notifyError(error))
.do(() => {
this.usersItems.nextBy(v => v.replaceAll(x => x.id === id, u => u.unlock()));
});
}
public search(filter: string) {
this.usersPager.nextBy(v => new Pager(0));
this.usersQuery.nextBy(v => filter);
this.loadUsers();
}
public goNext() {
this.usersPager.nextBy(v => v.goNext());
this.loadUsers();
}
public goPrev() {
this.usersPager.nextBy(v => v.goPrev());
this.loadUsers();
}
public trackBy(index: number, user: UserDto): any {
return user.id;
}
private notifyError(error: string | ErrorDto) {
if (error instanceof ErrorDto) {
this.dialogs.notify(Notification.error(error.displayMessage));
} else {
this.dialogs.notify(Notification.error(error));
}
return Observable.throw(error);
}
}

10
src/Squidex/app/framework/utils/rxjs-extensions-impl.ts

@ -0,0 +1,10 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export function nextBy<T>(updater: (value: T) => T): void {
return this.next(updater(this.value));
}

20
src/Squidex/app/framework/utils/rxjs-extensions.ts

@ -0,0 +1,20 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { nextBy } from './rxjs-extensions-impl';
/* tslint:disable:no-shadowed-variable */
declare module 'rxjs/BehaviorSubject' {
interface BehaviorSubject<T> {
nextBy(updater: (value: T) => T): void;
}
}
BehaviorSubject.prototype['nextBy'] = nextBy;
Loading…
Cancel
Save