mirror of https://github.com/Squidex/squidex.git
17 changed files with 416 additions and 212 deletions
@ -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(); |
|||
}); |
|||
}); |
|||
@ -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); |
|||
@ -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> |
|||
Loading…
Reference in new issue