-
-
-
= 0">
-
Total Size
-
{{storageCurrent | sqxFileSize}}
-
0">Total limit: {{storageAllowed | sqxFileSize}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/app/features/dashboard/pages/dashboard-page.component.scss b/frontend/app/features/dashboard/pages/dashboard-page.component.scss
index 2c21b8727..67aaa36e1 100644
--- a/frontend/app/features/dashboard/pages/dashboard-page.component.scss
+++ b/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,81 +34,68 @@
margin-bottom: 0;
margin-top: -1rem;
}
-}
-
-.card-image {
- img {
- height: 5rem;
- }
-}
-.card {
- & {
- @include force-height(16rem);
- float: left;
- margin-bottom: 1rem;
- margin-right: 1rem;
- width: 16rem;
+ .subtext {
+ margin-bottom: 2rem;
}
- &-lg {
- width: 33rem;
- }
+ .card {
+ & {
+ @include force-height(16rem);
+ height: 100%;
+ }
- &-image {
- text-align: center;
- }
+ &-image {
+ text-align: center;
- &-history {
- overflow-y: auto;
- }
-
- &-text {
- color: $color-text-decent;
- font-size: .9rem;
- font-weight: normal;
- }
+ img {
+ height: 5rem;
+ }
+ }
- &-title {
- color: $color-title;
- font-size: 1.2rem;
- font-weight: light;
- margin-top: 1rem;
- }
+ h4 {
+ a {
+ color: $color-title;
+ }
+ }
- &-href {
- & {
- cursor: pointer;
+ &-history {
+ overflow-y: auto;
}
- &:hover {
- @include box-shadow-outer(0, 3px, 16px, .2px);
+ &-text {
+ color: $color-text-decent;
+ font-size: .9rem;
+ font-weight: normal;
}
- &:focus {
- outline: none;
+ &-title {
+ color: $color-title;
+ font-size: 1.2rem;
+ font-weight: light;
+ margin-top: 1rem;
}
- &:hover,
- &:focus,
- &:active {
- text-decoration: none;
+ &-href {
+ &:hover {
+ @include box-shadow-outer(0, 3px, 16px, .2px);
+ }
}
}
-}
-.aggregation {
- & {
- text-align: center;
- }
+ .aggregation {
+ & {
+ text-align: center;
+ }
- &-label {
- color: $color-text-decent;
- }
+ &-label {
+ color: $color-text-decent;
+ }
- &-value {
- font-size: 3rem;
- margin-bottom: .5rem;
- margin-top: 1rem;
+ &-value {
+ font-size: 3rem;
+ margin-bottom: .5rem;
+ margin-top: 1rem;
+ }
}
}
\ No newline at end of file
diff --git a/frontend/app/features/dashboard/pages/dashboard-page.component.ts b/frontend/app/features/dashboard/pages/dashboard-page.component.ts
index a3b88dc37..e8008172d 100644
--- a/frontend/app/features/dashboard/pages/dashboard-page.component.ts
+++ b/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
= [
- ' 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 = [
+ { 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 = [
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
- };
-
- public stackedChartOptions = {
- responsive: true,
- scales: {
- xAxes: [{
- display: true,
- stacked: true
- }],
- yAxes: [{
- ticks: {
- beginAtZero: true
- },
- stacked: true
- }]
- },
- maintainAspectRatio: false
- };
-
- public history: ReadonlyArray = [];
-
- public profileDisplayName = '';
-
- public chartStorageCount: any;
- public chartStorageSize: any;
- public chartCallsCount: any;
- public chartCallsBytes: any;
- public chartCallsPerformance: any;
-
- public storageCurrent = 0;
- public storageAllowed = 0;
-
- public callsPerformance = 0;
- public callsCurrent = 0;
- public callsAllowed = 0;
- public callsBytes = 0;
-
- public get isStacked() {
- return this.isStackedValue;
- }
+export class DashboardPageComponent extends ResourceOwner implements AfterViewInit, OnInit {
+ @ViewChild('grid')
+ public grid: GridsterComponent;
- public set isStacked(value: boolean) {
- this.localStore.setBoolean('dashboard.charts.stacked', value);
+ public isStacked: boolean;
- this.isStackedValue = value;
- }
+ public storageCurrent: CurrentStorageDto;
+ public storageUsage: ReadonlyArray;
+
+ public callsUsage: CallsUsageDto;
+
+ public gridConfig: GridsterItem[];
+ public gridModal = new ModalModel();
+ public gridOptions = DEFAULT_OPTIONS;
+
+ public allItems = DEFAULT_CONFIG;
+
+ 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)
- }))
- };
-
- 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)
- }))
- };
-
- 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)
- }))
- };
-
- this.callsPerformance = averageElapsedMs;
- this.callsBytes = totalBytes;
- this.callsCurrent = totalCalls;
- this.callsAllowed = allowedCalls;
+ .subscribe(dto => {
+ this.callsUsage = dto;
}));
}
- public downloadLog() {
- this.usagesService.getLog(this.appsState.appName)
- .subscribe(url => {
- window.open(url, '_blank');
- });
+ public ngAfterViewInit() {
+ this.renderer.listen(this.grid.el, 'scroll', () => {
+ this.isScrolled = this.grid.el.scrollTop > 0;
+ });
}
-}
-function label(category: string) {
- return category === '*' ? 'anonymous' : category;
-}
+ public changeIsStacked(value: boolean) {
+ this.localStore.setBoolean('dashboard.charts.stacked', value);
+
+ this.isStacked = value;
+ }
-function createLabels(dtos: ReadonlyArray<{ date: DateTime }>): ReadonlyArray {
- return dtos.map(d => d.date.toStringFormat('M-dd'));
-}
+ public resetConfig() {
+ this.gridConfig = [...this.allItems];
+
+ this.saveConfig();
+ }
-function createLabelsFromSet(dtos: { [category: string]: ReadonlyArray<{ date: DateTime }> }): ReadonlyArray {
- return createLabels(dtos[Object.keys(dtos)[0]]);
+ public saveConfig() {
+ this.uiState.set('dashboard.grid', this.gridConfig, true);
+
+ this.dialogs.notifyInfo('Configuration saved.');
+ }
+
+ public isSelected(item: GridsterItem) {
+ return this.gridConfig && this.gridConfig.find(x => x.type === item.type);
+ }
+
+ 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 });
+ }
+ }
+
+ public trackByItem(index: number, item: GridsterItem) {
+ return item.type;
+ }
}
\ No newline at end of file
diff --git a/frontend/app/framework/angular/forms/confirm-click.directive.ts b/frontend/app/framework/angular/forms/confirm-click.directive.ts
index 1cd2189d3..e954cae29 100644
--- a/frontend/app/framework/angular/forms/confirm-click.directive.ts
+++ b/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();
}
diff --git a/frontend/app/shared/state/ui.state.ts b/frontend/app/shared/state/ui.state.ts
index 9549bb75b..879d1480e 100644
--- a/frontend/app/shared/state/ui.state.ts
+++ b/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 {
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 {
) {
super({
settings: {},
- settingsCommon: {},
- settingsShared: {},
- settingsUser: {}
+ settingsCommon: {}
});
this.loadResources();
@@ -103,7 +101,12 @@ export class UIState extends State {
}
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 => {
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e59289eec..024061231 100644
--- a/frontend/package-lock.json
+++ b/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",
diff --git a/frontend/package.json b/frontend/package.json
index ae605c2bd..0947f5b34 100644
--- a/frontend/package.json
+++ b/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",