Browse Source

Pipes / Performance.

pull/282/head
Sebastian Stehle 8 years ago
parent
commit
06c46fa4ff
  1. 4
      src/Squidex/app/features/assets/pages/assets-page.component.html
  2. 6
      src/Squidex/app/features/assets/pages/assets-page.component.ts
  3. 2
      src/Squidex/app/features/content/shared/contents-selector.component.html
  4. 6
      src/Squidex/app/features/content/shared/contents-selector.component.ts
  5. 1
      src/Squidex/app/features/rules/declarations.ts
  6. 2
      src/Squidex/app/features/rules/module.ts
  7. 26
      src/Squidex/app/features/rules/pages/events/pipes.ts
  8. 8
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.html
  9. 12
      src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts
  10. 7
      src/Squidex/app/features/settings/declarations.ts
  11. 4
      src/Squidex/app/features/settings/module.ts
  12. 110
      src/Squidex/app/features/settings/pages/backups/backups-page.component.html
  13. 40
      src/Squidex/app/features/settings/pages/backups/backups-page.component.ts
  14. 41
      src/Squidex/app/features/settings/pages/backups/pipes.ts
  15. 12
      src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts
  16. 10
      src/Squidex/app/framework/angular/pipes/date-time.pipes.ts
  17. 3
      src/Squidex/app/framework/module.ts
  18. 1
      src/Squidex/app/shared/internal.ts
  19. 2
      src/Squidex/app/shared/module.ts
  20. 75
      src/Squidex/app/shared/state/backups.state.spec.ts
  21. 69
      src/Squidex/app/shared/state/backups.state.ts

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

@ -6,11 +6,11 @@
</ng-container>
<ng-container menu>
<button class="btn btn-link btn-secondary" (click)="load(true)" title="Refresh Assets (CTRL + SHIFT + R)">
<button class="btn btn-link btn-secondary" (click)="reload()" title="Refresh Assets (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>
<sqx-shortcut keys="ctrl+shift+f" (trigger)="inputFind.focus()"></sqx-shortcut>
<form class="form-inline" (ngSubmit)="search()">

6
src/Squidex/app/features/assets/pages/assets-page.component.ts

@ -27,11 +27,11 @@ export class AssetsPageComponent implements OnInit {
}
public ngOnInit() {
this.load();
this.assetsState.load().onErrorResumeNext().subscribe();
}
public load(notify = false) {
this.assetsState.load(notify).onErrorResumeNext().subscribe();
public reload() {
this.assetsState.load(true).onErrorResumeNext().subscribe();
}
public search() {

2
src/Squidex/app/features/content/shared/contents-selector.component.html

@ -5,7 +5,7 @@
<ng-container tabs>
<div class="text-right">
<button class="btn btn-link btn-secondary" (click)="load(true)">
<button class="btn btn-link btn-secondary" (click)="reload()">
<i class="icon-reset"></i> Refresh
</button>

6
src/Squidex/app/features/content/shared/contents-selector.component.ts

@ -56,7 +56,11 @@ export class ContentsSelectorComponent implements OnInit {
this.load();
}
public load(showInfo = false) {
public reload() {
this.load(true);
}
private load(showInfo = false) {
this.contentsService.getContents(this.appsState.appName, this.schema.name, this.contentsPager.pageSize, this.contentsPager.skip, this.contentsQuery, undefined, false)
.finally(() => {
this.selectedItems = {};

1
src/Squidex/app/features/rules/declarations.ts

@ -16,4 +16,5 @@ export * from './pages/rules/triggers/content-changed-trigger.component';
export * from './pages/rules/rule-wizard.component';
export * from './pages/rules/rules-page.component';
export * from './pages/events/pipes';
export * from './pages/events/rule-events-page.component';

2
src/Squidex/app/features/rules/module.ts

@ -21,6 +21,7 @@ import {
ContentChangedTriggerComponent,
ElasticSearchActionComponent,
FastlyActionComponent,
RuleEventBadgeClassPipe,
RuleEventsPageComponent,
RulesPageComponent,
RuleWizardComponent,
@ -61,6 +62,7 @@ const routes: Routes = [
ContentChangedTriggerComponent,
ElasticSearchActionComponent,
FastlyActionComponent,
RuleEventBadgeClassPipe,
RuleEventsPageComponent,
RulesPageComponent,
RuleWizardComponent,

26
src/Squidex/app/features/rules/pages/events/pipes.ts

@ -0,0 +1,26 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sqxRuleEventBadgeClass',
pure: true
})
export class RuleEventBadgeClassPipe implements PipeTransform {
public transform(status: string) {
if (status === 'Retry') {
return 'warning';
} else if (status === 'Failed') {
return 'danger';
} else if (status === 'Pending') {
return 'secondary';
} else {
return status.toLowerCase();
}
}
}

8
src/Squidex/app/features/rules/pages/events/rule-events-page.component.html

@ -29,9 +29,7 @@
<th class="cell-time">
Created
</th>
<th class="cell-actions">
</th>
<th class="cell-actions"></th>
</tr>
</thead>
@ -39,7 +37,7 @@
<ng-template ngFor let-event [ngForOf]="eventsItems">
<tr [class.expanded]="selectedEventId === event.id">
<td class="cell-label">
<span class="badge badge-pill badge-{{getBadgeClass(event.jobResult)}}">{{event.jobResult}}</span>
<span class="badge badge-pill badge-{{event.jobResult | sqxRuleEventBadgeClass}}">{{event.jobResult}}</span>
</td>
<td class="cell-40">
<span class="truncate">{{event.eventName}}</span>
@ -64,7 +62,7 @@
<div class="row event-stats">
<div class="col-3">
<span class="badge badge-pill badge-{{getBadgeClass(event.result)}}">{{event.result}}</span>
<span class="badge badge-pill badge-{{event.result | sqxRuleEventBadgeClass}}">{{event.result}}</span>
</div>
<div class="col-3">
Attempts: {{event.numCalls}}

12
src/Squidex/app/features/rules/pages/events/rule-events-page.component.ts

@ -76,17 +76,5 @@ export class RuleEventsPageComponent implements OnInit {
this.load();
}
public getBadgeClass(status: string) {
if (status === 'Retry') {
return 'warning';
} else if (status === 'Failed') {
return 'danger';
} else if (status === 'Pending') {
return 'secondary';
} else {
return status.toLowerCase();
}
}
}

7
src/Squidex/app/features/settings/declarations.ts

@ -6,14 +6,21 @@
*/
export * from './pages/backups/backups-page.component';
export * from './pages/backups/pipes';
export * from './pages/clients/client.component';
export * from './pages/clients/clients-page.component';
export * from './pages/contributors/contributors-page.component';
export * from './pages/languages/language.component';
export * from './pages/languages/languages-page.component';
export * from './pages/more/more-page.component';
export * from './pages/patterns/pattern.component';
export * from './pages/patterns/patterns-page.component';
export * from './pages/plans/plans-page.component';
export * from './settings-area.component';

4
src/Squidex/app/features/settings/module.ts

@ -15,6 +15,8 @@ import {
} from '@app/shared';
import {
BackupDownloadUrlPipe,
BackupDurationPipe,
BackupsPageComponent,
ClientComponent,
ClientsPageComponent,
@ -76,6 +78,8 @@ const routes: Routes = [
RouterModule.forChild(routes)
],
declarations: [
BackupDownloadUrlPipe,
BackupDurationPipe,
BackupsPageComponent,
ClientComponent,
ClientsPageComponent,

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

@ -6,75 +6,77 @@
</ng-container>
<ng-container menu>
<button class="btn btn-success" [disabled]="backups.length === 10" (click)="startBackup()">
<button class="btn btn-success" [disabled]="backupsState.maxBackupsReached | async" (click)="startBackup()">
Start Backup
</button>
</ng-container>
<ng-container content>
<div class="panel-alert panel-alert-danger" *ngIf="backups.length >= 10">
<div class="panel-alert panel-alert-danger" *ngIf="backupsState.maxBackupsReached | async">
Your have reached the maximum number of backups: 10.
</div>
<div class="table-items-row table-items-row-empty" *ngIf="backups.length === 0">
No backups created yet.
</div>
<ng-container *ngIf="backupsState.backups | async; let backups">
<div class="table-items-row table-items-row-empty" *ngIf="backups.length === 0">
No backups created yet.
</div>
<div class="table-items-row" *ngFor="let backup of backups; trackBy: trackByBackup">
<div class="row no-gutter">
<div class="col col-auto">
<div *ngIf="!backup.stopped" class="backup-status backup-status-pending spin">
<i class="icon-hour-glass"></i>
<div class="table-items-row" *ngFor="let backup of backups; trackBy: trackByBackup">
<div class="row no-gutter">
<div class="col col-auto">
<div *ngIf="!backup.stopped" class="backup-status backup-status-pending spin">
<i class="icon-hour-glass"></i>
</div>
<div *ngIf="backup.stopped && backup.isFailed" class="backup-status backup-status-failed">
<i class="icon-exclamation"></i>
</div>
<div *ngIf="backup.stopped && !backup.isFailed" class="backup-status backup-status-success">
<i class="icon-checkmark"></i>
</div>
</div>
<div *ngIf="backup.stopped && backup.isFailed" class="backup-status backup-status-failed">
<i class="icon-exclamation"></i>
<div class="col col-auto">
<div>
Started:
</div>
<div>
Duration:
</div>
</div>
<div *ngIf="backup.stopped && !backup.isFailed" class="backup-status backup-status-success">
<i class="icon-checkmark"></i>
</div>
</div>
<div class="col col-auto">
<div>
Started:
</div>
<div>
Duration:
</div>
</div>
<div class="col col-auto">
<div>
{{backup.started.toISOString()}}
<div class="col col-auto">
<div>
{{backup.started | sqxISODate}}
</div>
<div *ngIf="backup.stopped">
{{backup | sqxBackupDuration}}
</div>
</div>
<div *ngIf="backup.stopped">
{{getDuration(backup) | sqxDuration}}
</div>
</div>
<div class="col">
<div>
<span title="Archived events">
Events: <strong class="backup-progress">{{backup.handledEvents | sqxKNumber}}</strong>
</span>,
<span title="Archived assets">
Assets: <strong class="backup-progress">{{backup.handledAssets | sqxKNumber}}</strong>
</span>
</div>
<div *ngIf="backup.stopped && !backup.isFailed">
Download:
<div class="col">
<div>
<span title="Archived events">
Events: <strong class="backup-progress">{{backup.handledEvents | sqxKNumber}}</strong>
</span>,
<span title="Archived assets">
Assets: <strong class="backup-progress">{{backup.handledAssets | sqxKNumber}}</strong>
</span>
</div>
<div *ngIf="backup.stopped && !backup.isFailed">
Download:
<a href="{{getDownloadUrl(backup)}}" target="_blank">
Ready
</a>
<a href="{{backup | sqxBackupDownloadUrl}}" target="_blank">
Ready
</a>
</div>
</div>
<div class="col col-auto">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleteBackup(backup)"
confirmTitle="Delete backup"
confirmText="Do you really want to delete the backup?">
<i class="icon-bin2"></i>
</button>
</div>
</div>
<div class="col col-auto">
<button type="button" class="btn btn-link btn-danger"
(sqxConfirmClick)="deleteBackup(backup)"
confirmTitle="Delete backup"
confirmText="Do you really want to delete the backup?">
<i class="icon-bin2"></i>
</button>
</div>
</div>
</div>
</ng-container>
</ng-container>
</sqx-panel>

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

@ -9,13 +9,9 @@ import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import {
ApiUrlConfig,
AppsState,
BackupDto,
BackupsService,
Duration,
DialogService,
ImmutableArray
BackupsState
} from '@app/shared';
@Component({
@ -26,13 +22,9 @@ import {
export class BackupsPageComponent implements OnInit, OnDestroy {
private loadSubscription: Subscription;
public backups = ImmutableArray.empty<BackupDto>();
constructor(
public readonly appsState: AppsState,
private readonly apiUrl: ApiUrlConfig,
private readonly backupsService: BackupsService,
private readonly dialogs: DialogService
public readonly backupsState: BackupsState
) {
}
@ -43,36 +35,16 @@ export class BackupsPageComponent implements OnInit, OnDestroy {
public ngOnInit() {
this.loadSubscription =
Observable.timer(0, 2000)
.switchMap(t => this.backupsService.getBackups(this.appsState.appName))
.subscribe(dtos => {
this.backups = ImmutableArray.of(dtos);
});
.switchMap(t => this.backupsState.load().onErrorResumeNext())
.subscribe();
}
public startBackup() {
this.backupsService.postBackup(this.appsState.appName)
.subscribe(() => {
this.dialogs.notifyInfo('Backup started, it can take several minutes to complete.');
}, error => {
this.dialogs.notifyError(error);
});
this.backupsState.start().onErrorResumeNext().subscribe();
}
public deleteBackup(backup: BackupDto) {
this.backupsService.deleteBackup(this.appsState.appName, backup.id)
.subscribe(() => {
this.dialogs.notifyInfo('Backup is about to be deleted.');
}, error => {
this.dialogs.notifyError(error);
});
}
public getDownloadUrl(backup: BackupDto) {
return this.apiUrl.buildUrl(`api/apps/${this.appsState.appName}/backups/${backup.id}`);
}
public getDuration(backup: BackupDto) {
return Duration.create(backup.started, backup.stopped!);
this.backupsState.delete(backup).onErrorResumeNext().subscribe();
}
public trackByBackup(index: number, item: BackupDto) {

41
src/Squidex/app/features/settings/pages/backups/pipes.ts

@ -0,0 +1,41 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Pipe, PipeTransform } from '@angular/core';
import {
ApiUrlConfig,
AppsState,
BackupDto,
Duration
} from '@app/shared';
@Pipe({
name: 'sqxBackupDuration',
pure: true
})
export class BackupDurationPipe implements PipeTransform {
public transform(backup: BackupDto) {
return Duration.create(backup.started, backup.stopped!).toString();
}
}
@Pipe({
name: 'sqxBackupDownloadUrl',
pure: true
})
export class BackupDownloadUrlPipe implements PipeTransform {
constructor(
private readonly apiUrl: ApiUrlConfig,
private readonly appsState: AppsState
) {
}
public transform(backup: BackupDto) {
return this.apiUrl.buildUrl(`api/apps/${this.appsState.appName}/backups/${backup.id}`);
}
}

12
src/Squidex/app/framework/angular/pipes/date-time.pipes.spec.ts

@ -14,6 +14,7 @@ import {
DurationPipe,
FromNowPipe,
FullDateTimePipe,
ISODatePipe,
MonthPipe,
ShortDatePipe,
ShortTimePipe
@ -121,3 +122,14 @@ describe('ShortTimePipe', () => {
expect(actual).toBe(expected);
});
});
describe('ISODatePipe', () => {
it('should format to short time string', () => {
const pipe = new ISODatePipe();
const actual = pipe.transform(dateTime);
const expected = dateTime.toISOString();
expect(actual).toBe(expected);
});
});

10
src/Squidex/app/framework/angular/pipes/date-time.pipes.ts

@ -20,6 +20,16 @@ export class ShortDatePipe implements PipeTransform {
}
}
@Pipe({
name: 'sqxISODate',
pure: true
})
export class ISODatePipe implements PipeTransform {
public transform(value: DateTime): any {
return value.toISOString();
}
}
@Pipe({
name: 'sqxDate',
pure: true

3
src/Squidex/app/framework/module.ts

@ -35,6 +35,7 @@ import {
IgnoreScrollbarDirective,
ImageSourceDirective,
IndeterminateValueDirective,
ISODatePipe,
JscriptEditorComponent,
JsonEditorComponent,
KeysPipe,
@ -103,6 +104,7 @@ import {
IgnoreScrollbarDirective,
ImageSourceDirective,
IndeterminateValueDirective,
ISODatePipe,
JscriptEditorComponent,
JsonEditorComponent,
KeysPipe,
@ -160,6 +162,7 @@ import {
IgnoreScrollbarDirective,
ImageSourceDirective,
IndeterminateValueDirective,
ISODatePipe,
JscriptEditorComponent,
JsonEditorComponent,
KeysPipe,

1
src/Squidex/app/shared/internal.ts

@ -40,6 +40,7 @@ export * from './services/users.service';
export * from './state/apps.state';
export * from './state/assets.state';
export * from './state/backups.state';
export * from './state/clients.state';
export * from './state/contributors.state';
export * from './state/patterns.state';

2
src/Squidex/app/shared/module.ts

@ -32,6 +32,7 @@ import {
AuthInterceptor,
AuthService,
BackupsService,
BackupsState,
ClientsState,
ContentsService,
ContributorsState,
@ -140,6 +141,7 @@ export class SqxSharedModule {
AssetsService,
AuthService,
BackupsService,
BackupsState,
ClientsState,
ContentsService,
ContributorsState,

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

@ -0,0 +1,75 @@
/*
* 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 {
AppsState,
BackupDto,
BackupsState,
BackupsService,
DateTime,
DialogService
} from '@app/shared';
describe('BackupsState', () => {
const app = 'my-app';
const oldBackups = [
new BackupDto('id1', DateTime.now(), null, 1, 1, false),
new BackupDto('id2', DateTime.now(), null, 2, 2, false)
];
let dialogs: IMock<DialogService>;
let appsState: IMock<AppsState>;
let backupsService: IMock<BackupsService>;
let backupsState: BackupsState;
beforeEach(() => {
dialogs = Mock.ofType<DialogService>();
appsState = Mock.ofType<AppsState>();
appsState.setup(x => x.appName)
.returns(() => app);
backupsService = Mock.ofType<BackupsService>();
backupsService.setup(x => x.getBackups(app))
.returns(() => Observable.of(oldBackups));
backupsState = new BackupsState(appsState.object, backupsService.object, dialogs.object);
backupsState.load().subscribe();
});
it('should load clients', () => {
expect(backupsState.snapshot.backups.values).toEqual(oldBackups);
});
it('should not add backup to snapshot', () => {
backupsService.setup(x => x.postBackup(app))
.returns(() => Observable.of({}));
backupsState.start().subscribe();
expect(backupsState.snapshot.backups.length).toBe(2);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
it('should not remove backup from snapshot', () => {
backupsService.setup(x => x.deleteBackup(app, oldBackups[0].id))
.returns(() => Observable.of({}));
backupsState.delete(oldBackups[0]).subscribe();
expect(backupsState.snapshot.backups.length).toBe(2);
dialogs.verify(x => x.notifyInfo(It.isAnyString()), Times.once());
});
});

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

@ -0,0 +1,69 @@
/*
* 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/framework';
import { AppsState } from './apps.state';
import { BackupDto, BackupsService } from './../services/backups.service';
interface Snapshot {
backups: ImmutableArray<BackupDto>;
}
@Injectable()
export class BackupsState extends State<Snapshot> {
public backups =
this.changes.map(x => x.backups);
public maxBackupsReached =
this.changes.map(x => x.backups.length >= 10);
constructor(
private readonly appsState: AppsState,
private readonly backupsService: BackupsService,
private readonly dialogs: DialogService
) {
super({ backups: ImmutableArray.empty() });
}
public load(): Observable<any> {
return this.backupsService.getBackups(this.appName)
.do(dtos => {
this.next({ backups: ImmutableArray.of(dtos) });
});
}
public start(): Observable<any> {
return this.backupsService.postBackup(this.appsState.appName)
.do(() => {
this.dialogs.notifyInfo('Backup started, it can take several minutes to complete.');
})
.notify(this.dialogs);
}
public delete(backup: BackupDto): Observable<any> {
return this.backupsService.deleteBackup(this.appsState.appName, backup.id)
.do(() => {
this.dialogs.notifyInfo('Backup is about to be deleted.');
})
.notify(this.dialogs);
}
private get appName() {
return this.appsState.appName;
}
}
Loading…
Cancel
Save