Browse Source

Feature/grid (#539)

* Seperate components.

* Configurable dashboard.

* Final fixes.
pull/540/head
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
f6ac2bc219
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      frontend/app/features/dashboard/declarations.ts
  2. 20
      frontend/app/features/dashboard/module.ts
  3. 14
      frontend/app/features/dashboard/pages/cards/api-calls-card.component.html
  4. 0
      frontend/app/features/dashboard/pages/cards/api-calls-card.component.scss
  5. 60
      frontend/app/features/dashboard/pages/cards/api-calls-card.component.ts
  6. 10
      frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.html
  7. 0
      frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.scss
  8. 36
      frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.ts
  9. 15
      frontend/app/features/dashboard/pages/cards/api-card.component.html
  10. 0
      frontend/app/features/dashboard/pages/cards/api-card.component.scss
  11. 23
      frontend/app/features/dashboard/pages/cards/api-card.component.ts
  12. 20
      frontend/app/features/dashboard/pages/cards/api-performance-card.component.html
  13. 0
      frontend/app/features/dashboard/pages/cards/api-performance-card.component.scss
  14. 61
      frontend/app/features/dashboard/pages/cards/api-performance-card.component.ts
  15. 20
      frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html
  16. 0
      frontend/app/features/dashboard/pages/cards/api-traffic-card.component.scss
  17. 61
      frontend/app/features/dashboard/pages/cards/api-traffic-card.component.ts
  18. 6
      frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.html
  19. 0
      frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.scss
  20. 51
      frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.ts
  21. 6
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.html
  22. 0
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.scss
  23. 51
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.ts
  24. 10
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.html
  25. 0
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.scss
  26. 36
      frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.ts
  27. 15
      frontend/app/features/dashboard/pages/cards/github-card.component.html
  28. 0
      frontend/app/features/dashboard/pages/cards/github-card.component.scss
  29. 23
      frontend/app/features/dashboard/pages/cards/github-card.component.ts
  30. 6
      frontend/app/features/dashboard/pages/cards/history-card.component.html
  31. 0
      frontend/app/features/dashboard/pages/cards/history-card.component.scss
  32. 35
      frontend/app/features/dashboard/pages/cards/history-card.component.ts
  33. 27
      frontend/app/features/dashboard/pages/cards/schema-card.component.html
  34. 0
      frontend/app/features/dashboard/pages/cards/schema-card.component.scss
  35. 23
      frontend/app/features/dashboard/pages/cards/schema-card.component.ts
  36. 80
      frontend/app/features/dashboard/pages/cards/shared.ts
  37. 15
      frontend/app/features/dashboard/pages/cards/support-card.component.html
  38. 0
      frontend/app/features/dashboard/pages/cards/support-card.component.scss
  39. 23
      frontend/app/features/dashboard/pages/cards/support-card.component.ts
  40. 235
      frontend/app/features/dashboard/pages/dashboard-page.component.html
  41. 64
      frontend/app/features/dashboard/pages/dashboard-page.component.scss
  42. 266
      frontend/app/features/dashboard/pages/dashboard-page.component.ts
  43. 9
      frontend/app/framework/angular/forms/confirm-click.directive.ts
  44. 21
      frontend/app/shared/state/ui.state.ts
  45. 8
      frontend/package-lock.json
  46. 1
      frontend/package.json

12
frontend/app/features/dashboard/declarations.ts

@ -5,4 +5,16 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
export * from './pages/cards/api-calls-card.component';
export * from './pages/cards/api-calls-summary-card.component';
export * from './pages/cards/api-card.component';
export * from './pages/cards/api-performance-card.component';
export * from './pages/cards/api-traffic-card.component';
export * from './pages/cards/asset-uploads-count-card.component';
export * from './pages/cards/asset-uploads-size-card.component';
export * from './pages/cards/asset-uploads-size-summary-card.component';
export * from './pages/cards/github-card.component';
export * from './pages/cards/history-card.component';
export * from './pages/cards/schema-card.component';
export * from './pages/cards/support-card.component';
export * from './pages/dashboard-page.component';

20
frontend/app/features/dashboard/module.ts

@ -5,11 +5,14 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
// tslint:disable: max-line-length
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { SqxFrameworkModule, SqxSharedModule } from '@app/shared';
import { GridsterModule } from 'angular-gridster2';
import { ChartModule } from 'angular2-chartjs';
import { DashboardPageComponent } from './declarations';
import { ApiCallsCardComponent, ApiCallsSummaryCardComponent, ApiCardComponent, ApiPerformanceCardComponent, ApiTrafficCardComponent, AssetUploadsCountCardComponent, AssetUploadsSizeCardComponent, AssetUploadsSizeSummaryCardComponent, DashboardPageComponent, GithubCardComponent, HistoryCardComponent, SchemaCardComponent, SupportCardComponent } from './declarations';
const routes: Routes = [
{
@ -21,12 +24,25 @@ const routes: Routes = [
@NgModule({
imports: [
ChartModule,
GridsterModule,
SqxFrameworkModule,
SqxSharedModule,
RouterModule.forChild(routes)
],
declarations: [
DashboardPageComponent
ApiCallsCardComponent,
ApiCallsSummaryCardComponent,
ApiCardComponent,
ApiPerformanceCardComponent,
ApiTrafficCardComponent,
AssetUploadsCountCardComponent,
AssetUploadsSizeCardComponent,
AssetUploadsSizeSummaryCardComponent,
DashboardPageComponent,
GithubCardComponent,
HistoryCardComponent,
SchemaCardComponent,
SupportCardComponent
]
})
export class SqxFeatureDashboardModule {}

14
frontend/app/features/dashboard/pages/cards/api-calls-card.component.html

@ -0,0 +1,14 @@
<div class="card card-lg">
<div class="card-header">
API Calls
<div class="float-right">
<a class="force" (click)="downloadLog()">
<small>Download Log</small>
</a>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartData" [options]="chartOptions"></chart>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/api-calls-card.component.scss

60
frontend/app/features/dashboard/pages/cards/api-calls-card.component.ts

@ -0,0 +1,60 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { AppDto, CallsUsageDto, fadeAnimation, UsagesService } from '@app/shared';
import { ChartHelpers, ChartOptions } from './shared';
@Component({
selector: 'sqx-api-calls-card',
styleUrls: ['./api-calls-card.component.scss'],
templateUrl: './api-calls-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiCallsCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: CallsUsageDto;
public chartOptions = ChartOptions.Stacked;
public chartData: any;
constructor(
private readonly usagesService: UsagesService
) {
}
public ngOnChanges() {
if (this.usage) {
const labels = ChartHelpers.createLabelsFromSet(this.usage.details);
this.chartData = {
labels,
datasets: Object.keys(this.usage.details).map((k, i) => (
{
label: ChartHelpers.label(k),
backgroundColor: ChartHelpers.getBackgroundColor(i),
borderColor: ChartHelpers.getBorderColor(i),
borderWidth: 1,
data: this.usage.details[k].map(x => x.averageElapsedMs)
}))
};
}
}
public downloadLog() {
this.usagesService.getLog(this.app.name)
.subscribe(url => {
window.open(url, '_blank');
});
}
}

10
frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.html

@ -0,0 +1,10 @@
<div class="card card">
<div class="card-header">API Calls</div>
<div class="card-body">
<div class="aggregation" *ngIf="callsTotal >= 0">
<div class="aggregation-label">This month</div>
<div class="aggregation-value">{{callsTotal | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsAllowed > 0">Monthly limit: {{callsAllowed | sqxKNumber}}</div>
</div>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.scss

36
frontend/app/features/dashboard/pages/cards/api-calls-summary-card.component.ts

@ -0,0 +1,36 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { AppDto, CallsUsageDto, fadeAnimation } from '@app/shared';
@Component({
selector: 'sqx-api-calls-summary-card',
styleUrls: ['./api-calls-summary-card.component.scss'],
templateUrl: './api-calls-summary-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiCallsSummaryCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: CallsUsageDto;
public callsTotal = 0;
public callsAllowed = 0;
public ngOnChanges() {
if (this.usage) {
this.callsTotal = this.usage.totalCalls;
this.callsAllowed = this.usage.allowedCalls;
}
}
}

15
frontend/app/features/dashboard/pages/cards/api-card.component.html

@ -0,0 +1,15 @@
<div class="card card-href">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-api.svg" />
</div>
<h4 class="card-title">
<a href="/api/content/{{app.name}}/docs" sqxExternalLink>Content API</a>
</h4>
<div class="card-text">
OpenAPI 3.0 compatible documentation for your app content.
</div>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/api-card.component.scss

23
frontend/app/features/dashboard/pages/cards/api-card.component.ts

@ -0,0 +1,23 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AppDto, fadeAnimation } from '@app/shared';
@Component({
selector: 'sqx-api-card',
styleUrls: ['./api-card.component.scss'],
templateUrl: './api-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiCardComponent {
@Input()
public app: AppDto;
}

20
frontend/app/features/dashboard/pages/cards/api-performance-card.component.html

@ -0,0 +1,20 @@
<div class="card card-lg">
<div class="card-header">
API Performance (ms): {{chartSummary}}ms avg
<div class="float-right">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked"
[ngModel]="isStacked"
(ngModelChange)="isStackedChange.emit($event)" />
<label class="form-check-label" for="stacked">
Stacked
</label>
</div>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartData" [options]="chartOptions"></chart>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/api-performance-card.component.scss

61
frontend/app/features/dashboard/pages/cards/api-performance-card.component.ts

@ -0,0 +1,61 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
import { AppDto, CallsUsageDto, fadeAnimation } from '@app/shared';
import { ChartHelpers, ChartOptions } from './shared';
@Component({
selector: 'sqx-api-performance-card',
styleUrls: ['./api-performance-card.component.scss'],
templateUrl: './api-performance-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiPerformanceCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: CallsUsageDto;
@Input()
public isStacked = false;
@Output()
public isStackedChange = new EventEmitter<boolean>();
public get chartOptions() {
return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default;
}
public chartData: any;
public chartSummary = 0;
public ngOnChanges() {
if (this.usage) {
const labels = ChartHelpers.createLabelsFromSet(this.usage.details);
this.chartData = {
labels,
datasets: Object.keys(this.usage.details).map((k, i) => (
{
label: ChartHelpers.label(k),
backgroundColor: ChartHelpers.getBackgroundColor(i),
borderColor: ChartHelpers.getBorderColor(i),
borderWidth: 1,
data: this.usage.details[k].map(x => x.totalCalls)
}))
};
this.chartSummary = this.usage.averageElapsedMs;
}
}
}

20
frontend/app/features/dashboard/pages/cards/api-traffic-card.component.html

@ -0,0 +1,20 @@
<div class="card card-lg">
<div class="card-header">
Traffic (MB): {{chartSummary | sqxFileSize}} total
<div class="float-right">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked"
[ngModel]="isStacked"
(ngModelChange)="isStackedChange.emit($event)" />
<label class="form-check-label" for="stacked">
Stacked
</label>
</div>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartData" [options]="chartOptions"></chart>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/api-traffic-card.component.scss

61
frontend/app/features/dashboard/pages/cards/api-traffic-card.component.ts

@ -0,0 +1,61 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { AppDto, CallsUsageDto, fadeAnimation } from '@app/shared';
import { ChartHelpers, ChartOptions } from './shared';
@Component({
selector: 'sqx-api-traffic-card',
styleUrls: ['./api-traffic-card.component.scss'],
templateUrl: './api-traffic-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ApiTrafficCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: CallsUsageDto;
@Input()
public isStacked = false;
@Output()
public isStackedChange = new EventEmitter<boolean>();
public get chartOptions() {
return this.isStacked ? ChartOptions.Stacked : ChartOptions.Default;
}
public chartData: any;
public chartSummary = 0;
public ngOnChanges(changes: SimpleChanges) {
if (this.usage && changes['usage']) {
const labels = ChartHelpers.createLabelsFromSet(this.usage.details);
this.chartData = {
labels,
datasets: Object.keys(this.usage.details).map((k, i) => (
{
label: ChartHelpers.label(k),
backgroundColor: ChartHelpers.getBackgroundColor(i),
borderColor: ChartHelpers.getBorderColor(i),
borderWidth: 1,
data: this.usage.details[k].map(x => Math.round(100 * (x.totalBytes / (1024 * 1024))) / 100)
}))
};
this.chartSummary = this.usage.totalBytes;
}
}
}

6
frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.html

@ -0,0 +1,6 @@
<div class="card card-lg">
<div class="card-header">Assets Uploads</div>
<div class="card-body">
<chart type="line" [data]="chartData" [options]="chartOptions"></chart>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.scss

51
frontend/app/features/dashboard/pages/cards/asset-uploads-count-card.component.ts

@ -0,0 +1,51 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { AppDto, fadeAnimation, StorageUsagePerDateDto } from '@app/shared';
import { ChartHelpers, ChartOptions } from './shared';
@Component({
selector: 'sqx-asset-uploads-count-card',
styleUrls: ['./asset-uploads-count-card.component.scss'],
templateUrl: './asset-uploads-count-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetUploadsCountCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: ReadonlyArray<StorageUsagePerDateDto>;
public chartData: any;
public chartOptions = ChartOptions.Default;
public ngOnChanges() {
if (this.usage) {
const labels = ChartHelpers.createLabels(this.usage);
this.chartData = {
labels,
datasets: [
{
label: 'All',
lineTension: 0,
fill: false,
backgroundColor: ChartHelpers.getBackgroundColor(),
borderColor: ChartHelpers.getBorderColor(),
borderWidth: 1,
data: this.usage.map(x => x.totalCount)
}
]
};
}
}
}

6
frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.html

@ -0,0 +1,6 @@
<div class="card card-lg">
<div class="card-header">Assets Uploads</div>
<div class="card-body">
<chart type="line" [data]="chartData" [options]="chartOptions"></chart>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.scss

51
frontend/app/features/dashboard/pages/cards/asset-uploads-size-card.component.ts

@ -0,0 +1,51 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { AppDto, fadeAnimation, StorageUsagePerDateDto } from '@app/shared';
import { ChartHelpers, ChartOptions } from './shared';
@Component({
selector: 'sqx-asset-uploads-size-card',
styleUrls: ['./asset-uploads-size-card.component.scss'],
templateUrl: './asset-uploads-size-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetUploadsSizeCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: ReadonlyArray<StorageUsagePerDateDto>;
public chartData: any;
public chartOptions = ChartOptions.Default;
public ngOnChanges() {
if (this.usage) {
const labels = ChartHelpers.createLabels(this.usage);
this.chartData = {
labels,
datasets: [
{
label: 'All',
lineTension: 0,
fill: false,
backgroundColor: ChartHelpers.getBackgroundColor(),
borderColor: ChartHelpers.getBorderColor(),
borderWidth: 1,
data: this.usage.map(x => Math.round(100 * (x.totalSize / (1024 * 1024))) / 100)
}
]
};
}
}
}

10
frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.html

@ -0,0 +1,10 @@
<div class="card card">
<div class="card-header">Assets Size (MB)</div>
<div class="card-body">
<div class="aggregation" *ngIf="storageCurrent >= 0">
<div class="aggregation-label">Total Size</div>
<div class="aggregation-value">{{storageCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="storageAllowed > 0">Total limit: {{storageAllowed | sqxFileSize}}</div>
</div>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.scss

36
frontend/app/features/dashboard/pages/cards/asset-uploads-size-summary-card.component.ts

@ -0,0 +1,36 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { AppDto, CurrentStorageDto, fadeAnimation } from '@app/shared';
@Component({
selector: 'sqx-asset-uploads-size-summary-card',
styleUrls: ['./asset-uploads-size-summary-card.component.scss'],
templateUrl: './asset-uploads-size-summary-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AssetUploadsSizeSummaryCardComponent implements OnChanges {
@Input()
public app: AppDto;
@Input()
public usage: CurrentStorageDto;
public storageCurrent = 0;
public storageAllowed = 0;
public ngOnChanges() {
if (this.usage) {
this.storageCurrent = this.usage.size;
this.storageAllowed = this.usage.maxAllowed;
}
}
}

15
frontend/app/features/dashboard/pages/cards/github-card.component.html

@ -0,0 +1,15 @@
<div class="card card-href">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-github.svg" />
</div>
<h4 class="card-title">
<a href="https://github.com/squidex/squidex" sqxExternalLink>Github</a>
</h4>
<div class="card-text">
Get the source code from Github and report bugs or ask for support.
</div>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/github-card.component.scss

23
frontend/app/features/dashboard/pages/cards/github-card.component.ts

@ -0,0 +1,23 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AppDto, fadeAnimation } from '@app/shared';
@Component({
selector: 'sqx-github-card',
styleUrls: ['./github-card.component.scss'],
templateUrl: './github-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GithubCardComponent {
@Input()
public app: AppDto;
}

6
frontend/app/features/dashboard/pages/cards/history-card.component.html

@ -0,0 +1,6 @@
<div class="card card-lg">
<div class="card-header">History</div>
<div class="card-body card-history card-body-scroll">
<sqx-history-list [events]="history | async"></sqx-history-list>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/history-card.component.scss

35
frontend/app/features/dashboard/pages/cards/history-card.component.ts

@ -0,0 +1,35 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
import { AppDto, fadeAnimation, HistoryEventDto, HistoryService } from '@app/shared';
import { Observable } from 'rxjs';
@Component({
selector: 'sqx-history-card',
styleUrls: ['./history-card.component.scss'],
templateUrl: './history-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HistoryCardComponent implements OnChanges {
@Input()
public app: AppDto;
public history: Observable<ReadonlyArray<HistoryEventDto>>;
constructor(
private readonly historyService: HistoryService
) {
}
public ngOnChanges() {
this.history = this.historyService.getHistory(this.app.name, '');
}
}

27
frontend/app/features/dashboard/pages/cards/schema-card.component.html

@ -0,0 +1,27 @@
<div class="card card-href">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-schema.svg" />
</div>
<ng-container *ngIf="app.canReadSchemas; else noPermission">
<h4 class="card-title">
<a [routerLink]="['schemas', { showDialog: true }]">New Schema</a>
</h4>
<div class="card-text">
A schema defines the structure of your content element.
</div>
</ng-container>
<ng-template #noPermission>
<h4 class="card-title">
<a [routerLink]="['schemas']">Schemas</a>
</h4>
<div class="card-text">
Get an insight to the data model of this app.
</div>
</ng-template>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/schema-card.component.scss

23
frontend/app/features/dashboard/pages/cards/schema-card.component.ts

@ -0,0 +1,23 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AppDto, fadeAnimation } from '@app/shared';
@Component({
selector: 'sqx-schema-card',
styleUrls: ['./schema-card.component.scss'],
templateUrl: './schema-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SchemaCardComponent {
@Input()
public app: AppDto;
}

80
frontend/app/features/dashboard/pages/cards/shared.ts

@ -0,0 +1,80 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { DateTime } from '@app/shared';
const ColorSchema: ReadonlyArray<string> = [
' 51, 137, 213',
'211, 50, 50',
'131, 211, 50',
' 50, 211, 131',
' 50, 211, 211',
' 50, 131, 211',
' 50, 50, 211',
' 50, 211, 50',
'131, 50, 211',
'211, 50, 211',
'211, 50, 131'
];
export module ChartHelpers {
export function label(category: string) {
return category === '*' ? 'anonymous' : category;
}
export function createLabels(dtos: ReadonlyArray<{ date: DateTime }>): ReadonlyArray<string> {
return dtos.map(d => d.date.toStringFormat('M-dd'));
}
export function createLabelsFromSet(dtos: { [category: string]: ReadonlyArray<{ date: DateTime }> }): ReadonlyArray<string> {
return createLabels(dtos[Object.keys(dtos)[0]]);
}
export function getBackgroundColor(i = 0) {
return `rgba(${ColorSchema[i]}, 0.6)`;
}
export function getBorderColor(i = 0) {
return `rgba(${ColorSchema[i]}, 1)`;
}
}
export module ChartOptions {
export const Default = {
responsive: true,
scales: {
xAxes: [{
display: true,
stacked: false
}],
yAxes: [{
ticks: {
beginAtZero: true
},
stacked: false
}]
},
maintainAspectRatio: false
};
export const Stacked = {
responsive: true,
scales: {
xAxes: [{
display: true,
stacked: true
}],
yAxes: [{
ticks: {
beginAtZero: true
},
stacked: true
}]
},
maintainAspectRatio: false
};
}

15
frontend/app/features/dashboard/pages/cards/support-card.component.html

@ -0,0 +1,15 @@
<div class="card card-href">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-feedback.svg" />
</div>
<h4 class="card-title">
<a href="https://support.squidex.io" sqxExternalLink>Feedback & Support</a>
</h4>
<div class="card-text">
Provide feedback and request features to help us to improve Squidex.
</div>
</div>
</div>

0
frontend/app/features/dashboard/pages/cards/support-card.component.scss

23
frontend/app/features/dashboard/pages/cards/support-card.component.ts

@ -0,0 +1,23 @@
/*
* Squidex Headless CMS
*
* @license
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import { AppDto, fadeAnimation } from '@app/shared';
@Component({
selector: 'sqx-support-card',
styleUrls: ['./support-card.component.scss'],
templateUrl: './support-card.component.html',
animations: [
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SupportCardComponent {
@Input()
public app: AppDto;
}

235
frontend/app/features/dashboard/pages/dashboard-page.component.html

@ -2,8 +2,7 @@
<ng-container *ngIf="appsState.selectedApp | async; let app">
<div class="dashboard" @fade>
<div class="dashboard-inner">
<div>
<div class="dashboard-summary" *ngIf="!isScrolled" @fade>
<h1 class="dashboard-title">Hi {{authState.user?.displayName}}</h1>
<div class="subtext">
@ -11,157 +10,111 @@
</div>
</div>
<div class="clearfix">
<a class="card card-href" [routerLink]="['schemas', { showDialog: true }]" *ngIf="app.canCreateSchema">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-schema.svg" />
</div>
<h4 class="card-title">New Schema</h4>
<div class="card-text">
A schema defines the structure of your content element.
</div>
</div>
</a>
<a class="card card-href" href="/api/content/{{appsState.appName}}/docs" sqxExternalLink="noicon">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-api.svg" />
</div>
<h4 class="card-title">Content API <i class="icon-external-link"></i></h4>
<div class="card-text">
OpenAPI 3.0 compatible documentation for your app content.
</div>
</div>
</a>
<a class="card card-href" href="https://support.squidex.io" sqxExternalLink="noicon">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-feedback.svg" />
</div>
<h4 class="card-title">Feedback & Support <i class="icon-external-link"></i></h4>
<div class="card-text">
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" sqxExternalLink="noicon">
<div class="card-body">
<div class="card-image">
<img src="./images/dashboard-github.svg" />
</div>
<h4 class="card-title">Github <i class="icon-external-link"></i></h4>
<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-header">
API Calls
<gridster [options]="gridOptions" #grid>
<gridster-item [item]="item" *ngFor="let item of gridConfig; trackBy: trackByItem">
<ng-container [ngSwitch]="item.type">
<ng-container *ngSwitchCase="'schemas'">
<sqx-schema-card
[app]="app">
</sqx-schema-card>
</ng-container>
<ng-container *ngSwitchCase="'api'">
<sqx-api-card
[app]="app">
</sqx-api-card>
</ng-container>
<ng-container *ngSwitchCase="'support'">
<sqx-support-card
[app]="app">
</sqx-support-card>
</ng-container>
<ng-container *ngSwitchCase="'github'">
<sqx-github-card
[app]="app">
</sqx-github-card>
</ng-container>
<ng-container *ngSwitchCase="'api-calls'">
<sqx-api-calls-card
[app]="app" [usage]="callsUsage">
</sqx-api-calls-card>
</ng-container>
<ng-container *ngSwitchCase="'api-performance'">
<sqx-api-performance-card
[isStacked]="isStacked"
(isStackedChange)="changeIsStacked($event)"
[app]="app" [usage]="callsUsage">
</sqx-api-performance-card>
</ng-container>
<ng-container *ngSwitchCase="'api-calls-summary'">
<sqx-api-calls-summary-card
[app]="app" [usage]="callsUsage">
</sqx-api-calls-summary-card>
</ng-container>
<ng-container *ngSwitchCase="'asset-uploads-count'">
<sqx-asset-uploads-count-card
[app]="app" [usage]="storageUsage">
</sqx-asset-uploads-count-card>
</ng-container>
<ng-container *ngSwitchCase="'asset-uploads-size-summary'">
<sqx-asset-uploads-size-summary-card
[app]="app" [usage]="storageCurrent">
</sqx-asset-uploads-size-summary-card>
</ng-container>
<ng-container *ngSwitchCase="'asset-uploads-size'">
<sqx-asset-uploads-size-card
[app]="app" [usage]="storageUsage">
</sqx-asset-uploads-size-card>
</ng-container>
<ng-container *ngSwitchCase="'api-traffic'">
<sqx-api-traffic-card
[isStacked]="isStacked"
(isStackedChange)="changeIsStacked($event)"
[app]="app" [usage]="callsUsage">
</sqx-api-traffic-card>
</ng-container>
<ng-container *ngSwitchCase="'history'">
<sqx-history-card
[app]="app">
</sqx-history-card>
</ng-container>
</ng-container>
</gridster-item>
</gridster>
<div class="float-right">
<a class="force" (click)="downloadLog()">
<small>Download Log</small>
</a>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartCallsCount" [options]="stackedChartOptions"></chart>
</div>
</div>
<div class="dashboard-settings">
<button type="button" class="btn settings-button" (click)="gridModal.toggle()" #buttonSettings>
<i class="icon-settings"></i>
</button>
<div class="card card-lg">
<div class="card-header">
API Performance (ms): {{callsPerformance}}ms avg
<ng-container *sqxModal="gridModal">
<div class="dropdown-menu" [sqxAnchoredTo]="buttonSettings" @fade position="bottom-right">
<div class="dropdown-item" *ngFor="let item of allItems">
<div class="float-right">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked" [(ngModel)]="isStacked" />
<label class="form-check-label" for="stacked">
Stacked
</label>
</div>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartCallsPerformance" [options]="isStacked ? stackedChartOptions : chartOptions"></chart>
</div>
</div>
<div class="card card">
<div class="card-header">API Calls</div>
<div class="card-body">
<div class="aggregation" *ngIf="callsCurrent >= 0">
<div class="aggregation-label">This month</div>
<div class="aggregation-value">{{callsCurrent | sqxKNumber}}</div>
<div class="aggregation-label" *ngIf="callsAllowed > 0">Monthly limit: {{callsAllowed | sqxKNumber}}</div>
</div>
</div>
</div>
<input class="form-check-input" type="checkbox" id="field_{{item.type}}"
[ngModel]="isSelected(item)"
(ngModelChange)="addOrRemove(item)" />
<div class="card card-lg">
<div class="card-header">Assets Uploads</div>
<div class="card-body">
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart>
<label class="form-check-label" for="field_{{item.type}}">
{{item.name}}
</label>
</div>
</div>
<div class="card card">
<div class="card-header">Assets Size (MB)</div>
<div class="card-body">
<div class="aggregation" *ngIf="storageCurrent >= 0">
<div class="aggregation-label">Total Size</div>
<div class="aggregation-value">{{storageCurrent | sqxFileSize}}</div>
<div class="aggregation-label" *ngIf="storageAllowed > 0">Total limit: {{storageAllowed | sqxFileSize}}</div>
</div>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="card card-lg">
<div class="card-header">Assets Uploads (MB)</div>
<div class="card-body">
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card-lg">
<div class="card-header">
Traffic (MB): {{callsBytes | sqxFileSize}} total
<a class="dropdown-item" (click)="saveConfig()">Save</a>
<div class="float-right">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="stacked" [(ngModel)]="isStacked" />
<label class="form-check-label" for="stacked">
Stacked
</label>
</div>
</div>
</div>
<div class="card-body">
<chart type="bar" [data]="chartCallsBytes" [options]="isStacked ? stackedChartOptions : chartOptions"></chart>
</div>
</div>
<div class="dropdown-divider"></div>
<div class="card card-lg">
<div class="card-header">History</div>
<div class="card-body card-history card-body-scroll">
<sqx-history-list [events]="history"></sqx-history-list>
</div>
</div>
<a class="dropdown-item dropdown-item-delete" (beforeClick)="gridModal.hide()"
(sqxConfirmClick)="resetConfig()"
confirmTitle="Reset config"
confirmText="Do you really want to reset the dashboard to the default?">
Reset
</a>
</div>
</ng-container>
</div>
</div>
</ng-container>

64
frontend/app/features/dashboard/pages/dashboard-page.component.scss

@ -1,22 +1,31 @@
.dashboard {
& {
@include absolute(0, 0, 0, 0);
overflow-y: auto;
}
&-summary {
@include absolute(2rem, null, null, 2rem);
}
&-settings {
@include absolute(1rem, 1rem, null, null);
}
&-title {
font-size: 1.4rem;
}
&-inner {
max-width: 75rem;
padding: 2rem;
padding-right: 1rem;
gridster {
background: none;
}
gridster-item {
overflow: visible;
}
}
.subtext {
margin-bottom: 2rem;
.btn {
z-index: 1000;
}
:host ::ng-deep {
@ -25,29 +34,29 @@
margin-bottom: 0;
margin-top: -1rem;
}
}
.card-image {
img {
height: 5rem;
}
.subtext {
margin-bottom: 2rem;
}
.card {
& {
@include force-height(16rem);
float: left;
margin-bottom: 1rem;
margin-right: 1rem;
width: 16rem;
}
&-lg {
width: 33rem;
height: 100%;
}
&-image {
text-align: center;
img {
height: 5rem;
}
}
h4 {
a {
color: $color-title;
}
}
&-history {
@ -68,23 +77,9 @@
}
&-href {
& {
cursor: pointer;
}
&:hover {
@include box-shadow-outer(0, 3px, 16px, .2px);
}
&:focus {
outline: none;
}
&:hover,
&:focus,
&:active {
text-decoration: none;
}
}
}
@ -103,3 +98,4 @@
margin-top: 1rem;
}
}
}

266
frontend/app/features/dashboard/pages/dashboard-page.component.ts

@ -5,24 +5,50 @@
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
*/
import { Component, OnInit } from '@angular/core';
import { AppsState, AuthService, DateTime, fadeAnimation, HistoryEventDto, HistoryService, LocalStoreService, ResourceOwner, UsagesService } from '@app/shared';
// tslint:disable: readonly-array
import { AfterViewInit, Component, OnInit, Renderer2, ViewChild } from '@angular/core';
import { AppsState, AuthService, CallsUsageDto, CurrentStorageDto, DateTime, DialogService, fadeAnimation, LocalStoreService, ModalModel, ResourceOwner, StorageUsagePerDateDto, UIState, UsagesService } from '@app/shared';
import { GridsterComponent, GridsterItem, GridType } from 'angular-gridster2';
import { switchMap } from 'rxjs/operators';
const COLORS: ReadonlyArray<string> = [
' 51, 137, 213',
'211, 50, 50',
'131, 211, 50',
' 50, 211, 131',
' 50, 211, 211',
' 50, 131, 211',
' 50, 50, 211',
' 50, 211, 50',
'131, 50, 211',
'211, 50, 211',
'211, 50, 131'
const DEFAULT_CONFIG: ReadonlyArray<GridsterItem> = [
{ cols: 1, rows: 1, x: 0, y: 0, type: 'schemas', name: 'Schema' },
{ cols: 1, rows: 1, x: 1, y: 0, type: 'api', name: 'API Documentation' },
{ cols: 1, rows: 1, x: 2, y: 0, type: 'support', name: 'Support' },
{ cols: 1, rows: 1, x: 3, y: 0, type: 'github', name: 'Github' },
{ cols: 2, rows: 1, x: 0, y: 1, type: 'api-calls', name: 'API Calls Chart' },
{ cols: 2, rows: 1, x: 2, y: 1, type: 'api-performance', name: 'API Performance Chart' },
{ cols: 1, rows: 1, x: 0, y: 2, type: 'api-calls-summary', name: 'API Calls Summary' },
{ cols: 2, rows: 1, x: 1, y: 2, type: 'asset-uploads-count', name: 'Asset Uploads Count Chart' },
{ cols: 1, rows: 1, x: 2, y: 2, type: 'asset-uploads-size-summary', name: 'Asset Uploads Size Chart' },
{ cols: 2, rows: 1, x: 0, y: 3, type: 'asset-uploads-size', name: 'Asset Total Storage Size' },
{ cols: 2, rows: 1, x: 2, y: 3, type: 'api-traffic', name: 'API Traffic Chart' },
{ cols: 2, rows: 1, x: 0, y: 4, type: 'history', name: 'History' }
];
const DEFAULT_OPTIONS = {
displayGrid: 'onDrag&Resize',
fixedColWidth: 254,
fixedRowHeight: 254,
gridType: GridType.Fixed,
outerMargin: true,
outerMarginBottom: 16,
outerMarginLeft: 16,
outerMarginRight: 16,
outerMarginTop: 120,
draggable: {
enabled: true
},
resizable: {
enabled: false
}
};
@Component({
selector: 'sqx-dashboard-page',
styleUrls: ['./dashboard-page.component.scss'],
@ -31,204 +57,110 @@ const COLORS: ReadonlyArray<string> = [
fadeAnimation
]
})
export class DashboardPageComponent extends ResourceOwner implements OnInit {
private isStackedValue: boolean;
public chartOptions = {
responsive: true,
scales: {
xAxes: [{
display: true,
stacked: false
}],
yAxes: [{
ticks: {
beginAtZero: true
},
stacked: false
}]
},
maintainAspectRatio: false
};
export class DashboardPageComponent extends ResourceOwner implements AfterViewInit, OnInit {
@ViewChild('grid')
public grid: GridsterComponent;
public stackedChartOptions = {
responsive: true,
scales: {
xAxes: [{
display: true,
stacked: true
}],
yAxes: [{
ticks: {
beginAtZero: true
},
stacked: true
}]
},
maintainAspectRatio: false
};
public history: ReadonlyArray<HistoryEventDto> = [];
public isStacked: boolean;
public profileDisplayName = '';
public storageCurrent: CurrentStorageDto;
public storageUsage: ReadonlyArray<StorageUsagePerDateDto>;
public chartStorageCount: any;
public chartStorageSize: any;
public chartCallsCount: any;
public chartCallsBytes: any;
public chartCallsPerformance: any;
public callsUsage: CallsUsageDto;
public storageCurrent = 0;
public storageAllowed = 0;
public gridConfig: GridsterItem[];
public gridModal = new ModalModel();
public gridOptions = DEFAULT_OPTIONS;
public callsPerformance = 0;
public callsCurrent = 0;
public callsAllowed = 0;
public callsBytes = 0;
public get isStacked() {
return this.isStackedValue;
}
public allItems = DEFAULT_CONFIG;
public set isStacked(value: boolean) {
this.localStore.setBoolean('dashboard.charts.stacked', value);
this.isStackedValue = value;
}
public isScrolled = false;
constructor(
public readonly appsState: AppsState,
public readonly authState: AuthService,
private readonly historyService: HistoryService,
private readonly renderer: Renderer2,
private readonly dialogs: DialogService,
private readonly usagesService: UsagesService,
private readonly localStore: LocalStoreService
private readonly localStore: LocalStoreService,
private readonly uiState: UIState
) {
super();
this.isStackedValue = localStore.getBoolean('dashboard.charts.stacked');
this.isStacked = localStore.getBoolean('dashboard.charts.stacked');
}
public ngOnInit() {
const dateTo = DateTime.today().toStringFormat('yyyy-MM-dd');
const dateFrom = DateTime.today().addDays(-20).toStringFormat('yyyy-MM-dd');
this.own(
this.appsState.selectedApp.pipe(
switchMap(app => this.usagesService.getTodayStorage(app.name)))
this.uiState.getUser('dashboard.grid', DEFAULT_CONFIG)
.subscribe(dto => {
this.storageCurrent = dto.size;
this.storageAllowed = dto.maxAllowed;
this.gridConfig = [...dto];
}));
this.own(
this.appsState.selectedApp.pipe(
switchMap(app => this.historyService.getHistory(app.name, '')))
switchMap(app => this.usagesService.getTodayStorage(app.name)))
.subscribe(dto => {
this.history = dto;
this.storageCurrent = dto;
}));
const dateTo = DateTime.today().toStringFormat('yyyy-MM-dd');
const dateFrom = DateTime.today().addDays(-20).toStringFormat('yyyy-MM-dd');
this.own(
this.appsState.selectedApp.pipe(
switchMap(app => this.usagesService.getStorageUsages(app.name, dateFrom, dateTo)))
.subscribe(dtos => {
const labels = createLabels(dtos);
this.chartStorageCount = {
labels,
datasets: [
{
label: 'All',
lineTension: 0,
fill: false,
backgroundColor: `rgba(${COLORS[0]}, 0.6)`,
borderColor: `rgba(${COLORS[0]}, 1)`,
borderWidth: 1,
data: dtos.map(x => x.totalCount)
}
]
};
this.chartStorageSize = {
labels,
datasets: [
{
label: 'All',
lineTension: 0,
fill: false,
backgroundColor: `rgba(${COLORS[0]}, 0.6)`,
borderColor: `rgba(${COLORS[0]}, 1)`,
borderWidth: 1,
data: dtos.map(x => Math.round(100 * (x.totalSize / (1024 * 1024))) / 100)
}
]
};
this.storageUsage = dtos;
}));
this.own(
this.appsState.selectedApp.pipe(
switchMap(app => this.usagesService.getCallsUsages(app.name, dateFrom, dateTo)))
.subscribe(({ details, totalBytes, totalCalls, allowedCalls, averageElapsedMs }) => {
const labels = createLabelsFromSet(details);
this.chartCallsCount = {
labels,
datasets: Object.keys(details).map((k, i) => (
{
label: label(k),
backgroundColor: `rgba(${COLORS[i]}, 0.6)`,
borderColor: `rgba(${COLORS[i]}, 1)`,
borderWidth: 1,
data: details[k].map(x => x.totalCalls)
}))
};
.subscribe(dto => {
this.callsUsage = dto;
}));
}
this.chartCallsBytes = {
labels,
datasets: Object.keys(details).map((k, i) => (
{
label: label(k),
backgroundColor: `rgba(${COLORS[i]}, 0.6)`,
borderColor: `rgba(${COLORS[i]}, 1)`,
borderWidth: 1,
data: details[k].map(x => Math.round(100 * (x.totalBytes / (1024 * 1024))) / 100)
}))
};
public ngAfterViewInit() {
this.renderer.listen(this.grid.el, 'scroll', () => {
this.isScrolled = this.grid.el.scrollTop > 0;
});
}
this.chartCallsPerformance = {
labels,
datasets: Object.keys(details).map((k, i) => (
{
label: label(k),
backgroundColor: `rgba(${COLORS[i]}, 0.6)`,
borderColor: `rgba(${COLORS[i]}, 1)`,
borderWidth: 1,
data: details[k].map(x => x.averageElapsedMs)
}))
};
public changeIsStacked(value: boolean) {
this.localStore.setBoolean('dashboard.charts.stacked', value);
this.callsPerformance = averageElapsedMs;
this.callsBytes = totalBytes;
this.callsCurrent = totalCalls;
this.callsAllowed = allowedCalls;
}));
this.isStacked = value;
}
public downloadLog() {
this.usagesService.getLog(this.appsState.appName)
.subscribe(url => {
window.open(url, '_blank');
});
public resetConfig() {
this.gridConfig = [...this.allItems];
this.saveConfig();
}
public saveConfig() {
this.uiState.set('dashboard.grid', this.gridConfig, true);
this.dialogs.notifyInfo('Configuration saved.');
}
function label(category: string) {
return category === '*' ? 'anonymous' : category;
public isSelected(item: GridsterItem) {
return this.gridConfig && this.gridConfig.find(x => x.type === item.type);
}
function createLabels(dtos: ReadonlyArray<{ date: DateTime }>): ReadonlyArray<string> {
return dtos.map(d => d.date.toStringFormat('M-dd'));
public addOrRemove(item: GridsterItem) {
const found = this.gridConfig.find(x => x.type === item.type);
if (found) {
this.gridConfig.splice(this.gridConfig.indexOf(found), 1);
} else {
this.gridConfig.push({ ...item });
}
}
function createLabelsFromSet(dtos: { [category: string]: ReadonlyArray<{ date: DateTime }> }): ReadonlyArray<string> {
return createLabels(dtos[Object.keys(dtos)[0]]);
public trackByItem(index: number, item: GridsterItem) {
return item.type;
}
}

9
frontend/app/framework/angular/forms/confirm-click.directive.ts

@ -50,6 +50,9 @@ export class ConfirmClickDirective implements OnDestroy {
@Input()
public confirmRequired = true;
@Output()
public beforeClick = new EventEmitter();
@Output('sqxConfirmClick')
public clickConfirmed = new DelayEventEmitter();
@ -76,12 +79,14 @@ export class ConfirmClickDirective implements OnDestroy {
this.isOpen = true;
this.beforeClick.emit();
const subscription =
this.dialogs.confirm(this.confirmTitle, this.confirmText)
.subscribe(confiormed => {
.subscribe(confirmed => {
this.isOpen = false;
if (confiormed) {
if (confirmed) {
this.clickConfirmed.delayEmit();
}

21
frontend/app/shared/state/ui.state.ts

@ -7,7 +7,7 @@
import { Injectable } from '@angular/core';
import { hasAnyLink, State, Types } from '@app/framework';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { UIService, UISettingsDto } from './../services/ui.service';
import { UsersService } from './../services/users.service';
import { AppsState } from './apps.state';
@ -17,10 +17,10 @@ interface Snapshot {
settingsCommon: object & any;
// All shared app settings.
settingsShared: object & any;
settingsShared?: object & any;
// All user app settings.
settingsUser: object & any;
settingsUser?: object & any;
// The merged settings of app and common settings.
settings: object & any;
@ -47,10 +47,10 @@ export class UIState extends State<Snapshot> {
this.project(x => x.settings);
public settingsShared =
this.project(x => x.settingsShared);
this.project(x => x.settingsShared).pipe(filter(x => !!x));
public settingsUser =
this.project(x => x.settingsUser);
this.project(x => x.settingsUser).pipe(filter(x => !!x));
public canReadEvents =
this.project(x => x.canReadEvents === true);
@ -89,9 +89,7 @@ export class UIState extends State<Snapshot> {
) {
super({
settings: {},
settingsCommon: {},
settingsShared: {},
settingsUser: {}
settingsCommon: {}
});
this.loadResources();
@ -103,7 +101,12 @@ export class UIState extends State<Snapshot> {
}
private load(app: string) {
this.next(s => updateSettings(s, {}));
this.next(s => ({
...s,
settings: s.settingsCommon,
settingsShared: undefined,
settingsUser: undefined
}));
this.uiService.getSharedSettings(app)
.subscribe(payload => {

8
frontend/package-lock.json

@ -941,6 +941,14 @@
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
},
"angular-gridster2": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/angular-gridster2/-/angular-gridster2-9.2.0.tgz",
"integrity": "sha512-69TVHDQhX8ZfCWHcmGumY/oSJJo/ihJ0kj2Nj6tZAk5JdbjmOntABpLYMlridU5Vfhq4tZhbaFHm6JlLrjj1cA==",
"requires": {
"tslib": "^1.10.0"
}
},
"angular-mentions": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/angular-mentions/-/angular-mentions-1.1.4.tgz",

1
frontend/package.json

@ -27,6 +27,7 @@
"@angular/platform-server": "9.0.6",
"@angular/router": "9.0.6",
"ace-builds": "^1.4.11",
"angular-gridster2": "^9.2.0",
"angular-mentions": "1.1.4",
"angular2-chartjs": "0.5.1",
"babel-polyfill": "6.26.0",

Loading…
Cancel
Save