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" @fade> |
||||
<div class="dashboard-inner"> |
<div class="dashboard-inner"> |
||||
|
<div> |
||||
<div> |
<h1 class="dashboard-title">Hi {{authState.user?.displayName}}</h1> |
||||
<h1 class="dashboard-title">Hi {{ctx.user.displayName}}</h1> |
|
||||
|
|
||||
<div class="subtext"> |
<div class="subtext"> |
||||
Welcome to <span class="app-name">{{(app | async).name}}</span> dashboard. |
Welcome to <span class="app-name">{{app.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> |
</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"> |
<h4 class="card-title">New Schema</h4> |
||||
A schema defines the structure of your content element. |
|
||||
</div> |
|
||||
</div> |
|
||||
</a> |
|
||||
|
|
||||
<a class="card card-href" href="/api/docs" target="_blank"> |
<div class="card-text"> |
||||
<div class="card-body"> |
A schema defines the structure of your content element. |
||||
<div class="card-image"> |
</div> |
||||
<img src="/images/dashboard-api.png" /> |
</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"> |
<h4 class="card-title">API Documentation</h4> |
||||
Swagger compatible documentation for app management. |
|
||||
</div> |
|
||||
</div> |
|
||||
</a> |
|
||||
|
|
||||
<a class="card card-href" (click)="showForum()"> |
<div class="card-text"> |
||||
<div class="card-body"> |
Swagger compatible documentation for app management. |
||||
<div class="card-image"> |
</div> |
||||
<img src="/images/dashboard-feedback.png" /> |
</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"> |
<h4 class="card-title">Feedback</h4> |
||||
Provide feedback and request features to help us to improve Squidex. |
|
||||
</div> |
|
||||
</div> |
|
||||
</a> |
|
||||
|
|
||||
<a class="card card-href" href="https://github.com/squidex/squidex" target="_blank"> |
<div class="card-text"> |
||||
<div class="card-body"> |
Provide feedback and request features to help us to improve Squidex. |
||||
<div class="card-image"> |
</div> |
||||
<img src="/images/dashboard-github.png" /> |
</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"> |
<h4 class="card-title">Github</h4> |
||||
Get the source code from Github and report bugs or ask for support. |
|
||||
</div> |
|
||||
</div> |
|
||||
</a> |
|
||||
|
|
||||
<div class="card card-lg"> |
<div class="card-text"> |
||||
<div class="card-body"> |
Get the source code from Github and report bugs or ask for support. |
||||
<chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart> |
</div> |
||||
</div> |
</div> |
||||
</div> |
</a> |
||||
|
|
||||
<div class="card card-lg"> |
<div class="card card-lg"> |
||||
<div class="card-body"> |
<div class="card-body"> |
||||
<chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart> |
<chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart> |
||||
</div> |
</div> |
||||
</div> |
</div> |
||||
|
|
||||
<div class="card card"> |
<div class="card card-lg"> |
||||
<div class="card-body"> |
<div class="card-body"> |
||||
<div class="aggregation" *ngIf="callsCurrent >= 0"> |
<chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart> |
||||
<div class="aggregation-label">API calls this month</div> |
</div> |
||||
<div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div> |
|
||||
<div class="aggregation-label" *ngIf="callsMax > 0">Monthly limit: {{callsMax | sqxKNumber}}</div> |
|
||||
</div> |
</div> |
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="card card-lg"> |
<div class="card card"> |
||||
<div class="card-body"> |
<div class="card-body"> |
||||
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart> |
<div class="aggregation" *ngIf="callsCurrent >= 0"> |
||||
</div> |
<div class="aggregation-label">API calls this month</div> |
||||
</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 card-lg"> |
||||
<div class="card-body"> |
<div class="card-body"> |
||||
<div class="aggregation" *ngIf="assetsCurrent >= 0"> |
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart> |
||||
<div class="aggregation-label">Asset size today</div> |
</div> |
||||
<div class="aggregation-value">{{assetsCurrent | sqxFileSize}}</div> |
|
||||
<div class="aggregation-label" *ngIf="assetsMax > 0">Total limit: {{assetsMax | sqxFileSize}}</div> |
|
||||
</div> |
</div> |
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<div class="card card-lg"> |
<div class="card card"> |
||||
<div class="card-body"> |
<div class="card-body"> |
||||
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart> |
<div class="aggregation" *ngIf="assetsCurrent >= 0"> |
||||
</div> |
<div class="aggregation-label">Asset size today</div> |
||||
</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 card-lg"> |
||||
<div class="card-header"> |
<div class="card-body"> |
||||
History |
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart> |
||||
</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> |
||||
<div class="event-main"> |
</div> |
||||
<div class="event-message"> |
|
||||
<span class="event-actor user-ref">{{event.actor | sqxUserNameRef:null}}</span> <span [innerHTML]="format(event.message) | async"></span> |
<div class="card card-lg"> |
||||
</div> |
<div class="card-header"> |
||||
<div class="event-created">{{event.created | sqxFromNow}}</div> |
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> |
</div> |
||||
</div> |
</div> |
||||
</div> |
</ng-container> |
||||
Loading…
Reference in new issue