diff --git a/src/Squidex/app/features/administration/declarations.ts b/src/Squidex/app/features/administration/declarations.ts index 6be8c6e4f..451d9dc50 100644 --- a/src/Squidex/app/features/administration/declarations.ts +++ b/src/Squidex/app/features/administration/declarations.ts @@ -17,4 +17,5 @@ export * from './pages/users/users-page.component'; export * from './services/event-consumers.service'; export * from './services/users.service'; +export * from './state/event-consumers.state'; export * from './state/users.state'; \ No newline at end of file diff --git a/src/Squidex/app/features/administration/module.ts b/src/Squidex/app/features/administration/module.ts index 1da69495c..12de77ff8 100644 --- a/src/Squidex/app/features/administration/module.ts +++ b/src/Squidex/app/features/administration/module.ts @@ -17,6 +17,7 @@ import { AdministrationAreaComponent, EventConsumersPageComponent, EventConsumersService, + EventConsumersState, UnsetUserGuard, UserMustExistGuard, UserPageComponent, @@ -73,6 +74,7 @@ const routes: Routes = [ ], providers: [ EventConsumersService, + EventConsumersState, UnsetUserGuard, UserMustExistGuard, UsersService, diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html index 3d18511a7..876db4046 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html @@ -6,11 +6,11 @@ - - + @@ -30,7 +30,7 @@ - + diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts index 645e3c8ee..810746d2f 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts @@ -8,13 +8,10 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; -import { - DialogService, - ImmutableArray, - ModalView -} from '@app/shared'; +import { ImmutableArray, ModalView } from '@app/shared'; -import { EventConsumerDto, EventConsumersService } from './../../services/event-consumers.service'; +import { EventConsumerDto } from './../../services/event-consumers.service'; +import { EventConsumersState } from '@appfeatures/administration/declarations'; @Component({ selector: 'sqx-event-consumers-page', @@ -22,87 +19,52 @@ import { EventConsumerDto, EventConsumersService } from './../../services/event- templateUrl: './event-consumers-page.component.html' }) export class EventConsumersPageComponent implements OnDestroy, OnInit { - private subscription: Subscription; + private timerSubscription: Subscription; public eventConsumerErrorDialog = new ModalView(); public eventConsumerError = ''; public eventConsumers = ImmutableArray.empty(); constructor( - private readonly dialogs: DialogService, - private readonly eventConsumersService: EventConsumersService + public readonly eventConsumersState: EventConsumersState ) { } public ngOnDestroy() { - this.subscription.unsubscribe(); + this.timerSubscription.unsubscribe(); } public ngOnInit() { - this.load(false, true); + this.eventConsumersState.load(false, true).onErrorResumeNext().subscribe(); - this.subscription = - Observable.timer(4000, 4000).subscribe(() => { - this.load(); - }); + this.timerSubscription = + Observable.timer(2000, 2000) + .switchMap(x => this.eventConsumersState.load().onErrorResumeNext()) + .subscribe(); } - public load(showInfo = false, showError = false) { - this.eventConsumersService.getEventConsumers() - .subscribe(dtos => { - this.eventConsumers = ImmutableArray.of(dtos); + public reload() { + this.eventConsumersState.load(true, true).onErrorResumeNext().subscribe(); + } - if (showInfo) { - this.dialogs.notifyInfo('Event Consumers reloaded.'); - } - }, error => { - if (showError) { - this.dialogs.notifyError(error); - } - }); + public start(es: EventConsumerDto) { + this.eventConsumersState.start(es).onErrorResumeNext().subscribe(); } - public start(consumer: EventConsumerDto) { - this.eventConsumersService.startEventConsumer(consumer.name) - .subscribe(() => { - this.eventConsumers = this.eventConsumers.replaceBy('name', start(consumer)); - }, error => { - this.dialogs.notifyError(error); - }); + public stop(es: EventConsumerDto) { + this.eventConsumersState.stop(es).onErrorResumeNext().subscribe(); } - public stop(consumer: EventConsumerDto) { - this.eventConsumersService.stopEventConsumer(consumer.name) - .subscribe(() => { - this.eventConsumers = this.eventConsumers.replaceBy('name', stop(consumer)); - }, error => { - this.dialogs.notifyError(error); - }); + public reset(es: EventConsumerDto) { + this.eventConsumersState.reset(es).onErrorResumeNext().subscribe(); } - public reset(consumer: EventConsumerDto) { - this.eventConsumersService.resetEventConsumer(consumer.name) - .subscribe(() => { - this.eventConsumers = this.eventConsumers.replaceBy('name', reset(consumer)); - }, error => { - this.dialogs.notifyError(error); - }); + public trackByEventConsumer(index: number, es: EventConsumerDto) { + return es.name; } public showError(eventConsumer: EventConsumerDto) { this.eventConsumerError = eventConsumer.error; this.eventConsumerErrorDialog.show(); } -} - -function start(es: EventConsumerDto) { - return new EventConsumerDto(es.name, false, false, es.error, es.position); -} - -function stop(es: EventConsumerDto) { - return new EventConsumerDto(es.name, true, false, es.error, es.position); -} - -function reset(es: EventConsumerDto) { - return new EventConsumerDto(es.name, es.isStopped, true, es.error, es.position); } \ No newline at end of file diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.html b/src/Squidex/app/features/administration/pages/users/users-page.component.html index 51b3dcab3..a5a65aa75 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.html +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.html @@ -62,10 +62,10 @@ - - diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts b/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts index 947619278..e870f1d24 100644 --- a/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts +++ b/src/Squidex/app/features/administration/services/event-consumers.service.spec.ts @@ -69,7 +69,7 @@ describe('EventConsumersService', () => { it('should make put request to start event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { - eventConsumersService.startEventConsumer('event-consumer1').subscribe(); + eventConsumersService.putStart('event-consumer1').subscribe(); const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/start'); @@ -82,7 +82,7 @@ describe('EventConsumersService', () => { it('should make put request to stop event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { - eventConsumersService.stopEventConsumer('event-consumer1').subscribe(); + eventConsumersService.putStop('event-consumer1').subscribe(); const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/stop'); @@ -95,7 +95,7 @@ describe('EventConsumersService', () => { it('should make put request to reset event consumer', inject([EventConsumersService, HttpTestingController], (eventConsumersService: EventConsumersService, httpMock: HttpTestingController) => { - eventConsumersService.resetEventConsumer('event-consumer1').subscribe(); + eventConsumersService.putReset('event-consumer1').subscribe(); const req = httpMock.expectOne('http://service/p/api/event-consumers/event-consumer1/reset'); diff --git a/src/Squidex/app/features/administration/services/event-consumers.service.ts b/src/Squidex/app/features/administration/services/event-consumers.service.ts index 71cc57b49..36f1dd234 100644 --- a/src/Squidex/app/features/administration/services/event-consumers.service.ts +++ b/src/Squidex/app/features/administration/services/event-consumers.service.ts @@ -53,21 +53,21 @@ export class EventConsumersService { .pretifyError('Failed to load event consumers. Please reload.'); } - public startEventConsumer(name: string): Observable { + public putStart(name: string): Observable { 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 { + public putStop(name: string): Observable { 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 { + public putReset(name: string): Observable { const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/reset`); return HTTP.putVersioned(this.http, url, {}) diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts new file mode 100644 index 000000000..3a50062da --- /dev/null +++ b/src/Squidex/app/features/administration/state/event-consumers.state.spec.ts @@ -0,0 +1,98 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Observable } from 'rxjs'; +import { IMock, It, Mock, Times } from 'typemoq'; + +import { DialogService } from '@app/shared'; + +import { EventConsumerDto, EventConsumersService } from './../services/event-consumers.service'; +import { EventConsumersState } from './event-consumers.state'; + +describe('EventConsumersState', () => { + const oldConsumers = [ + new EventConsumerDto('name1', false, false, 'error', '1'), + new EventConsumerDto('name2', true, true, 'error', '2') + ]; + + let dialogs: IMock; + let eventConsumersService: IMock; + let eventConsumersState: EventConsumersState; + + beforeEach(() => { + dialogs = Mock.ofType(); + + eventConsumersService = Mock.ofType(); + + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => Observable.of(oldConsumers)); + + eventConsumersState = new EventConsumersState(dialogs.object, eventConsumersService.object); + eventConsumersState.load().subscribe(); + }); + + it('should load event consumers', () => { + expect(eventConsumersState.snapshot.eventConsumers.values).toEqual(oldConsumers); + }); + + it('should show notification on load when flag is true', () => { + eventConsumersState.load(true, true).subscribe(); + + dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); + }); + + it('should show notification on load error when flag is true', () => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => Observable.throw({})); + + eventConsumersState.load(true, true).onErrorResumeNext().subscribe(); + + dialogs.verify(x => x.notifyError(It.isAny()), Times.once()); + }); + + it('should not show notification on load error when flag is false', () => { + eventConsumersService.setup(x => x.getEventConsumers()) + .returns(() => Observable.throw({})); + + eventConsumersState.load().onErrorResumeNext().subscribe(); + + dialogs.verify(x => x.notifyError(It.isAny()), Times.never()); + }); + + it('should mark consumer as started', () => { + eventConsumersService.setup(x => x.putStart(oldConsumers[1].name)) + .returns(() => Observable.of({})); + + eventConsumersState.start(oldConsumers[1]).subscribe(); + + const es_1 = eventConsumersState.snapshot.eventConsumers.at(1); + + expect(es_1.isStopped).toBeFalsy(); + }); + + it('should mark consumer as stopped', () => { + eventConsumersService.setup(x => x.putStop(oldConsumers[0].name)) + .returns(() => Observable.of({})); + + eventConsumersState.stop(oldConsumers[0]).subscribe(); + + const es_1 = eventConsumersState.snapshot.eventConsumers.at(0); + + expect(es_1.isStopped).toBeTruthy(); + }); + + it('should mark consumer as resetting', () => { + eventConsumersService.setup(x => x.putReset(oldConsumers[0].name)) + .returns(() => Observable.of({})); + + eventConsumersState.reset(oldConsumers[0]).subscribe(); + + const es_1 = eventConsumersState.snapshot.eventConsumers.at(0); + + expect(es_1.isResetting).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/src/Squidex/app/features/administration/state/event-consumers.state.ts b/src/Squidex/app/features/administration/state/event-consumers.state.ts new file mode 100644 index 000000000..e06d2a984 --- /dev/null +++ b/src/Squidex/app/features/administration/state/event-consumers.state.ts @@ -0,0 +1,99 @@ +/* + * Squidex Headless CMS + * + * @license + * Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved. + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +import '@app/framework/utils/rxjs-extensions'; + +import { + DialogService, + ImmutableArray, + State +} from '@app/shared'; + +import { EventConsumerDto, EventConsumersService } from './../services/event-consumers.service'; + +interface Snapshot { + eventConsumers: ImmutableArray; +} + +@Injectable() +export class EventConsumersState extends State { + public eventConsumers = + this.changes.map(x => x.eventConsumers); + + constructor( + private readonly dialogs: DialogService, + private readonly eventConsumersService: EventConsumersService + ) { + super({ eventConsumers: ImmutableArray.empty() }); + } + + public load(notifyLoad = false, notifyError = false): Observable { + return this.eventConsumersService.getEventConsumers() + .do(dtos => { + if (notifyLoad) { + this.dialogs.notifyInfo('Event consumers reloaded.'); + } + + this.next(s => { + const eventConsumers = ImmutableArray.of(dtos); + + return { ...s, eventConsumers }; + }); + }) + .catch(error => { + if (notifyError) { + this.dialogs.notifyError(error); + } + + return Observable.throw(error); + }); + } + + public start(es: EventConsumerDto): Observable { + return this.eventConsumersService.putStart(es.name) + .do(() => { + this.replaceEventConsumer(start(es)); + }) + .notify(this.dialogs); + } + + public stop(es: EventConsumerDto): Observable { + return this.eventConsumersService.putStop(es.name) + .do(() => { + this.replaceEventConsumer(stop(es)); + }) + .notify(this.dialogs); + } + + public reset(es: EventConsumerDto): Observable { + return this.eventConsumersService.putReset(es.name) + .do(() => { + this.replaceEventConsumer(reset(es)); + }) + .notify(this.dialogs); + } + + private replaceEventConsumer(es: EventConsumerDto) { + this.next(s => { + const eventConsumers = s.eventConsumers.replaceBy('name', es); + + return { ...s, eventConsumers }; + }); + } +} + +const start = (es: EventConsumerDto) => + new EventConsumerDto(es.name, false, false, es.error, es.position); + +const stop = (es: EventConsumerDto) => + new EventConsumerDto(es.name, true, false, es.error, es.position); + +const reset = (es: EventConsumerDto) => + new EventConsumerDto(es.name, es.isStopped, true, es.error, es.position); \ No newline at end of file diff --git a/src/Squidex/app/features/administration/state/users.state.spec.ts b/src/Squidex/app/features/administration/state/users.state.spec.ts index 70c8c3000..811f068cb 100644 --- a/src/Squidex/app/features/administration/state/users.state.spec.ts +++ b/src/Squidex/app/features/administration/state/users.state.spec.ts @@ -57,7 +57,7 @@ describe('UsersState', () => { usersService.verifyAll(); }); - it('should raise notification on load when notify is true', () => { + it('should show notification on load when flag is true', () => { usersState.load(true).subscribe(); dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once()); @@ -79,18 +79,7 @@ describe('UsersState', () => { expect(usersState.snapshot.selectedUser).toEqual(u(newUsers[0])); }); - it('should mark as current user when selected user equals to profile', () => { - let selectedUser: UserDto; - - usersState.select('id2').subscribe(x => { - selectedUser = x!; - }); - - expect(selectedUser!).toEqual(oldUsers[1]); - expect(usersState.snapshot.selectedUser).toEqual(u(oldUsers[1])); - }); - - it('should not load user when already loaded', () => { + it('should not load user on select when already loaded', () => { let selectedUser: UserDto; usersState.select('id1').subscribe(x => { @@ -103,7 +92,7 @@ describe('UsersState', () => { usersService.verify(x => x.getUser(It.isAnyString()), Times.never()); }); - it('should load user when not loaded', () => { + it('should load user on select when not loaded', () => { usersService.setup(x => x.getUser('id3')) .returns(() => Observable.of(newUser)); @@ -119,7 +108,7 @@ describe('UsersState', () => { usersService.verify(x => x.getUser('id3'), Times.once()); }); - it('should return null when unselecting user', () => { + it('should return null on select when unselecting user', () => { let selectedUser: UserDto; usersState.select(null).subscribe(x => { @@ -132,7 +121,7 @@ describe('UsersState', () => { usersService.verify(x => x.getUser(It.isAnyString()), Times.never()); }); - it('should return null when user to select is not found', () => { + it('should return null on select when user is not found', () => { usersService.setup(x => x.getUser('unknown')) .returns(() => Observable.throw({})); diff --git a/src/Squidex/app/features/administration/state/users.state.ts b/src/Squidex/app/features/administration/state/users.state.ts index edab26a4c..a58fdc912 100644 --- a/src/Squidex/app/features/administration/state/users.state.ts +++ b/src/Squidex/app/features/administration/state/users.state.ts @@ -127,10 +127,10 @@ export class UsersState extends State { }); } - public load(notify = false): Observable { + public load(notifyLoad = false): Observable { return this.usersService.getUsers(this.snapshot.usersPager.pageSize, this.snapshot.usersPager.skip, this.snapshot.usersQuery) .do(dtos => { - if (notify) { + if (notifyLoad) { this.dialogs.notifyInfo('Users reloaded.'); } diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html index ba5aa2a6e..623630b8a 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html @@ -1,134 +1,136 @@ - + + -
-
- -
-

Hi {{ctx.user.displayName}}

+
+
+
+

Hi {{authState.user?.displayName}}

-
- Welcome to {{(app | async).name}} dashboard. -
-
- -
- -
-
- +
+ Welcome to {{app.name}} dashboard.
+
-

New Schema

+
+ -
-
- -
-
+
+
+ +
+
-
-
-
-
API calls this month
-
{{callsCurrent | sqxKNumber}}
-
Monthly limit: {{callsMax | sqxKNumber}}
+
+
+ +
-
-
-
-
- -
-
+
+
+
+
API calls this month
+
{{callsCurrent | sqxKNumber}}
+
Monthly limit: {{callsMax | sqxKNumber}}
+
+
+
-
-
-
-
Asset size today
-
{{assetsCurrent | sqxFileSize}}
-
Total limit: {{assetsMax | sqxFileSize}}
+
+
+ +
-
-
-
-
- -
-
+
+
+
+
Asset size today
+
{{assetsCurrent | sqxFileSize}}
+
Total limit: {{assetsMax | sqxFileSize}}
+
+
+
-
-
- History -
-
-
-
- +
+
+
-
-
- {{event.actor | sqxUserNameRef:null}} -
-
{{event.created | sqxFromNow}}
+
+ +
+
+ History +
+
+
+
+ +
+
+
+ {{event.actor | sqxUserNameRef:null}} +
+
{{event.created | sqxFromNow}}
+
+
-
+
-
\ No newline at end of file + \ No newline at end of file diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts index 16232096e..7ef9eb5a8 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -9,8 +9,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { Observable, Subscription } from 'rxjs'; import { - AppContext, AppDto, + AppsState, + AuthService, DateTime, fadeAnimation, formatHistoryMessage, @@ -26,9 +27,6 @@ declare var _urq: any; selector: 'sqx-dashboard-page', styleUrls: ['./dashboard-page.component.scss'], templateUrl: './dashboard-page.component.html', - providers: [ - AppContext - ], animations: [ fadeAnimation ] @@ -43,7 +41,7 @@ export class DashboardPageComponent implements OnDestroy, OnInit { public chartCallsCount: any; public chartCallsPerformance: any; - public app = this.ctx.appChanges.filter(x => !!x).map(x => x); + public app = this.appsState.selectedApp.filter(x => !!x).map(x => x); public chartOptions = { responsive: true, @@ -72,7 +70,9 @@ export class DashboardPageComponent implements OnDestroy, OnInit { public callsCurrent = 0; public callsMax = 0; - constructor(public readonly ctx: AppContext, + constructor( + public readonly appsState: AppsState, + public readonly authState: AuthService, private readonly historyService: HistoryService, private readonly users: UsersProviderService, private readonly usagesService: UsagesService diff --git a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html index 3443c5e85..4b2f81ce8 100644 --- a/src/Squidex/app/features/settings/pages/backups/backups-page.component.html +++ b/src/Squidex/app/features/settings/pages/backups/backups-page.component.html @@ -6,7 +6,13 @@ - + + + + @@ -69,7 +75,7 @@