Browse Source

Event consumers state.

pull/282/head
Sebastian Stehle 8 years ago
parent
commit
cd8f429498
  1. 1
      src/Squidex/app/features/administration/declarations.ts
  2. 2
      src/Squidex/app/features/administration/module.ts
  3. 6
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html
  4. 82
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.ts
  5. 4
      src/Squidex/app/features/administration/pages/users/users-page.component.html
  6. 6
      src/Squidex/app/features/administration/services/event-consumers.service.spec.ts
  7. 6
      src/Squidex/app/features/administration/services/event-consumers.service.ts
  8. 98
      src/Squidex/app/features/administration/state/event-consumers.state.spec.ts
  9. 99
      src/Squidex/app/features/administration/state/event-consumers.state.ts
  10. 21
      src/Squidex/app/features/administration/state/users.state.spec.ts
  11. 4
      src/Squidex/app/features/administration/state/users.state.ts
  12. 214
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  13. 12
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  14. 10
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  15. 18
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  16. 26
      src/Squidex/app/shared/state/backups.state.spec.ts
  17. 19
      src/Squidex/app/shared/state/backups.state.ts

1
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';

2
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,

6
src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.html

@ -6,11 +6,11 @@
</ng-container>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Event Consumers (CTRL + SHIFT + R)">
<button class="btn btn-link btn-secondary" (click)="reload()" title="Refresh event consumers (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="load(true)"></sqx-shortcut>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
</ng-container>
<ng-container content>
@ -30,7 +30,7 @@
</thead>
<tbody>
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumers">
<ng-template ngFor let-eventConsumer [ngForOf]="eventConsumersState.eventConsumers | async" [ngForTrackBy]="trackByEventConsumer">
<tr [class.faulted]="eventConsumer.error && eventConsumer.error.length > 0">
<td class="auto-auto">
<span class="truncate">

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

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

@ -62,10 +62,10 @@
</td>
<td class="cell-actions">
<ng-container *ngIf="!userInfo.isCurrentUser">
<button class="btn btn-link" (click)="lock(user); $event.stopPropagation();" *ngIf="!userInfo.user.isLocked" title="Lock User">
<button class="btn btn-link" (click)="lock(userInfo.user); $event.stopPropagation();" *ngIf="!userInfo.user.isLocked" title="Lock User">
<i class="icon icon-unlocked"></i>
</button>
<button class="btn btn-link" (click)="unlock(user); $event.stopPropagation();" *ngIf="userInfo.user.isLocked" title="Unlock User">
<button class="btn btn-link" (click)="unlock(userInfo.user); $event.stopPropagation();" *ngIf="userInfo.user.isLocked" title="Unlock User">
<i class="icon icon-lock"></i>
</button>
</ng-container>

6
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');

6
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<any> {
public putStart(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> {
public putStop(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> {
public putReset(name: string): Observable<any> {
const url = this.apiUrl.buildUrl(`api/event-consumers/${name}/reset`);
return HTTP.putVersioned(this.http, url, {})

98
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<DialogService>;
let eventConsumersService: IMock<EventConsumersService>;
let eventConsumersState: EventConsumersState;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
eventConsumersService = Mock.ofType<EventConsumersService>();
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();
});
});

99
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<EventConsumerDto>;
}
@Injectable()
export class EventConsumersState extends State<Snapshot> {
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<any> {
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<any> {
return this.eventConsumersService.putStart(es.name)
.do(() => {
this.replaceEventConsumer(start(es));
})
.notify(this.dialogs);
}
public stop(es: EventConsumerDto): Observable<any> {
return this.eventConsumersService.putStop(es.name)
.do(() => {
this.replaceEventConsumer(stop(es));
})
.notify(this.dialogs);
}
public reset(es: EventConsumerDto): Observable<any> {
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);

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

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

@ -127,10 +127,10 @@ export class UsersState extends State<Snapshot> {
});
}
public load(notify = false): Observable<any> {
public load(notifyLoad = false): Observable<any> {
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.');
}

214
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -1,134 +1,136 @@
<sqx-title message="{app} | Dashboard" parameter1="app" [value1]="(app | async).name"></sqx-title>
<ng-container *ngIf="app | async; let app">
<sqx-title message="{app} | Dashboard" parameter1="app" [value1]="app.name"></sqx-title>
<div class="dashboard" @fade>
<div class="dashboard-inner">
<div>
<h1 class="dashboard-title">Hi {{ctx.user.displayName}}</h1>
<div class="dashboard" @fade>
<div class="dashboard-inner">
<div>
<h1 class="dashboard-title">Hi {{authState.user?.displayName}}</h1>
<div class="subtext">
Welcome to <span class="app-name">{{(app | async).name}}</span> dashboard.
</div>
</div>
<div class="clearfix">
<a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *ngIf="ctx.app.permission !== 'Editor'">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-schema.png" />
<div class="subtext">
Welcome to <span class="app-name">{{app.name}}</span> dashboard.
</div>
</div>
<h4 class="card-title">New Schema</h4>
<div class="clearfix">
<a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *ngIf="app.permission !== 'Editor'">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-schema.png" />
</div>
<div class="card-text">
A schema defines the structure of your content element.
</div>
</div>
</a>
<h4 class="card-title">New Schema</h4>
<a class="card card-href" href="/api/docs" target="_blank">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-api.png" />
</div>
<div class="card-text">
A schema defines the structure of your content element.
</div>
</div>
</a>
<h4 class="card-title">API Documentation</h4>
<a class="card card-href" href="/api/docs" target="_blank">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-api.png" />
</div>
<div class="card-text">
Swagger compatible documentation for app management.
</div>
</div>
</a>
<h4 class="card-title">API Documentation</h4>
<a class="card card-href" (click)="showForum()">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-feedback.png" />
</div>
<div class="card-text">
Swagger compatible documentation for app management.
</div>
</div>
</a>
<h4 class="card-title">Feedback</h4>
<a class="card card-href" (click)="showForum()">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-feedback.png" />
</div>
<div class="card-text">
Provide feedback and request features to help us to improve Squidex.
</div>
</div>
</a>
<h4 class="card-title">Feedback</h4>
<a class="card card-href" href="https://github.com/squidex/squidex" target="_blank">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-github.png" />
</div>
<div class="card-text">
Provide feedback and request features to help us to improve Squidex.
</div>
</div>
</a>
<h4 class="card-title">Github</h4>
<a class="card card-href" href="https://github.com/squidex/squidex" target="_blank">
<div class="card-body">
<div class="card-image">
<img src="/images/dashboard-github.png" />
</div>
<div class="card-text">
Get the source code from Github and report bugs or ask for support.
</div>
</div>
</a>
<h4 class="card-title">Github</h4>
<div class="card card-lg">
<div class="card-body">
<chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart>
</div>
</div>
<div class="card-text">
Get the source code from Github and report bugs or ask for support.
</div>
</div>
</a>
<div class="card card-lg">
<div class="card-body">
<chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card-lg">
<div class="card-body">
<chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card">
<div class="card-body">
<div class="aggregation" *ngIf="callsCurrent >= 0">
<div class="aggregation-label">API calls this month</div>
<div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsMax > 0">Monthly limit: {{callsMax | sqxKNumber}}</div>
<div class="card card-lg">
<div class="card-body">
<chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart>
</div>
</div>
</div>
</div>
<div class="card card-lg">
<div class="card-body">
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card">
<div class="card-body">
<div class="aggregation" *ngIf="callsCurrent >= 0">
<div class="aggregation-label">API calls this month</div>
<div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsMax > 0">Monthly limit: {{callsMax | sqxKNumber}}</div>
</div>
</div>
</div>
<div class="card card">
<div class="card-body">
<div class="aggregation" *ngIf="assetsCurrent >= 0">
<div class="aggregation-label">Asset size today</div>
<div class="aggregation-value">{{assetsCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="assetsMax > 0">Total limit: {{assetsMax | sqxFileSize}}</div>
<div class="card card-lg">
<div class="card-body">
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart>
</div>
</div>
</div>
</div>
<div class="card card-lg">
<div class="card-body">
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card">
<div class="card-body">
<div class="aggregation" *ngIf="assetsCurrent >= 0">
<div class="aggregation-label">Asset size today</div>
<div class="aggregation-value">{{assetsCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="assetsMax > 0">Total limit: {{assetsMax | sqxFileSize}}</div>
</div>
</div>
</div>
<div class="card card-lg">
<div class="card-header">
History
</div>
<div class="card-body card-history card-body-scroll">
<div *ngFor="let event of history" class="event">
<div class="event-left">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
<div class="card card-lg">
<div class="card-body">
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart>
</div>
<div class="event-main">
<div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
</div>
<div class="card card-lg">
<div class="card-header">
History
</div>
<div class="card-body card-history card-body-scroll">
<div *ngFor="let event of history" class="event">
<div class="event-left">
<img class="user-picture" [attr.title]="event.actor | sqxUserNameRef:null" [attr.src]="event.actor | sqxUserPictureRef" />
</div>
<div class="event-main">
<div class="event-message">
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span>
</div>
<div class="event-created">{{event.created | sqxFromNow}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</ng-container>

12
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 => <AppDto>x);
public app = this.appsState.selectedApp.filter(x => !!x).map(x => <AppDto>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

10
src/Squidex/app/features/settings/pages/backups/backups-page.component.html

@ -6,7 +6,13 @@
</ng-container>
<ng-container menu>
<button class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" (click)="startBackup()">
<button class="btn btn-link btn-secondary" (click)="reload()" title="Refresh backups (CTRL + SHIFT + R)">
<i class="icon-reset"></i> Refresh
</button>
<sqx-shortcut keys="ctrl+shift+r" (trigger)="reload()"></sqx-shortcut>
<button class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" (click)="start()">
Start Backup
</button>
</ng-container>
@ -69,7 +75,7 @@
</div>
<div class="col col-auto">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleteBackup(backup)"
(sqxConfirmClick)="delete(backup)"
confirmTitle="Delete backup"
confirmText="Do you really want to delete the backup?">
<i class="icon-bin2"></i>

18
src/Squidex/app/features/settings/pages/backups/backups-page.component.ts

@ -20,7 +20,7 @@ import {
templateUrl: './backups-page.component.html'
})
export class BackupsPageComponent implements OnInit, OnDestroy {
private loadSubscription: Subscription;
private timerSubscription: Subscription;
constructor(
public readonly appsState: AppsState,
@ -29,21 +29,27 @@ export class BackupsPageComponent implements OnInit, OnDestroy {
}
public ngOnDestroy() {
this.loadSubscription.unsubscribe();
this.timerSubscription.unsubscribe();
}
public ngOnInit() {
this.loadSubscription =
Observable.timer(0, 2000)
this.backupsState.load(false, true).onErrorResumeNext().subscribe();
this.timerSubscription =
Observable.timer(3000, 3000)
.switchMap(t => this.backupsState.load().onErrorResumeNext())
.subscribe();
}
public startBackup() {
public reload() {
this.backupsState.load(true, true).onErrorResumeNext().subscribe();
}
public start() {
this.backupsState.start().onErrorResumeNext().subscribe();
}
public deleteBackup(backup: BackupDto) {
public delete(backup: BackupDto) {
this.backupsState.delete(backup).onErrorResumeNext().subscribe();
}

26
src/Squidex/app/shared/state/backups.state.spec.ts

@ -47,10 +47,34 @@ describe('BackupsState', () => {
backupsState.load().subscribe();
});
it('should load clients', () => {
it('should load backups', () => {
expect(backupsState.snapshot.backups.values).toEqual(oldBackups);
});
it('should show notification on load when flag is true', () => {
backupsState.load(true, true).subscribe();
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should show notification on load error when flag is true', () => {
backupsService.setup(x => x.getBackups(app))
.returns(() => Observable.throw({}));
backupsState.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', () => {
backupsService.setup(x => x.getBackups(app))
.returns(() => Observable.throw({}));
backupsState.load().onErrorResumeNext().subscribe();
dialogs.verify(x => x.notifyError(It.isAny()), Times.never());
});
it('should not add backup to snapshot', () => {
backupsService.setup(x => x.postBackup(app))
.returns(() => Observable.of({}));

19
src/Squidex/app/shared/state/backups.state.ts

@ -40,10 +40,25 @@ export class BackupsState extends State<Snapshot> {
super({ backups: ImmutableArray.empty() });
}
public load(): Observable<any> {
public load(notifyLoad = false, notifyError = false): Observable<any> {
return this.backupsService.getBackups(this.appName)
.do(dtos => {
this.next({ backups: ImmutableArray.of(dtos) });
if (notifyLoad) {
this.dialogs.notifyInfo('Backups reloaded.');
}
this.next(s => {
const backups = ImmutableArray.of(dtos);
return { ...s, backups };
});
})
.catch(error => {
if (notifyError) {
this.dialogs.notifyError(error);
}
return Observable.throw(error);
});
}

Loading…
Cancel
Save