diff --git a/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs b/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs index fedc0ec5d..92dd64ce3 100644 --- a/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs +++ b/src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs @@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.MongoDb.UsageTracker Upsert); } - public async Task> FindAsync(string key, DateTime fromDate, DateTime toDate) + public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) { var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync(); diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 166fbd0cd..8c34b5e7f 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -104,13 +104,13 @@ namespace Squidex.Infrastructure.UsageTracking return TaskHelper.Done; } - public async Task> FindAsync(string key, DateTime fromDate, DateTime toDate) + public async Task> QueryAsync(string key, DateTime fromDate, DateTime toDate) { Guard.NotNull(key, nameof(key)); ThrowIfDisposed(); - var originalUsages = await usageStore.FindAsync(key, fromDate, toDate); + var originalUsages = await usageStore.QueryAsync(key, fromDate, toDate); var enrichedUsages = new List(); var usagesDictionary = originalUsages.ToDictionary(x => x.Date); @@ -130,7 +130,7 @@ namespace Squidex.Infrastructure.UsageTracking var dateFrom = new DateTime(date.Year, date.Month, 1); var dateTo = dateFrom.AddMonths(1).AddDays(-1); - var originalUsages = await usageStore.FindAsync(key, dateFrom, dateTo); + var originalUsages = await usageStore.QueryAsync(key, dateFrom, dateTo); return originalUsages.Sum(x => x.TotalCount); } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs index 0195cd6c1..1fe07c9cc 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs @@ -16,6 +16,6 @@ namespace Squidex.Infrastructure.UsageTracking { Task TrackUsagesAsync(DateTime date, string key, long count, long elapsedMs); - Task> FindAsync(string key, DateTime fromDate, DateTime toDate); + Task> QueryAsync(string key, DateTime fromDate, DateTime toDate); } } diff --git a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs index da4a8f45e..bbb6a291f 100644 --- a/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs @@ -18,6 +18,6 @@ namespace Squidex.Infrastructure.UsageTracking Task GetMonthlyCalls(string key, DateTime date); - Task> FindAsync(string key, DateTime fromDate, DateTime toDate); + Task> QueryAsync(string key, DateTime fromDate, DateTime toDate); } } diff --git a/src/Squidex/.sass-lint.yml b/src/Squidex/.sass-lint.yml index 177bc6bce..e753c8c91 100644 --- a/src/Squidex/.sass-lint.yml +++ b/src/Squidex/.sass-lint.yml @@ -1,5 +1,7 @@ rules: no-ids: + - 1 + no-important: - 0 final-newline: - 0 diff --git a/src/Squidex/Controllers/Api/Apps/AppUsageController.cs b/src/Squidex/Controllers/Api/Apps/AppUsageController.cs deleted file mode 100644 index 56ae8bfa1..000000000 --- a/src/Squidex/Controllers/Api/Apps/AppUsageController.cs +++ /dev/null @@ -1,92 +0,0 @@ -// ========================================================================== -// AppUsageController.cs -// Squidex Headless CMS -// ========================================================================== -// Copyright (c) Squidex Group -// All rights reserved. -// ========================================================================== - -using System; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using NSwag.Annotations; -using Squidex.Controllers.Api.Apps.Models; -using Squidex.Core.Identity; -using Squidex.Infrastructure.CQRS.Commands; -using Squidex.Infrastructure.UsageTracking; -using Squidex.Pipeline; - -namespace Squidex.Controllers.Api.Apps -{ - /// - /// Retrieves usage information for apps. - /// - [ApiExceptionFilter] - [ServiceFilter(typeof(AppFilterAttribute))] - [SwaggerTag("Apps")] - public class AppUsageController : ControllerBase - { - private readonly IUsageTracker usageTracker; - - public AppUsageController(ICommandBus commandBus, IUsageTracker usageTracker) - : base(commandBus) - { - this.usageTracker = usageTracker; - } - - /// - /// Get api calls for this month. - /// - /// The name of the app. - /// - /// 200 => Usage tracking results returned. - /// 404 => App not found. - /// - [Authorize(Roles = SquidexRoles.AppEditor)] - [HttpGet] - [Route("apps/{app}/usages/monthly")] - [ProducesResponseType(typeof(MonthlyCallsDto), 200)] - public async Task GetMonthlyCalls(string app) - { - var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today); - - return Ok(new MonthlyCallsDto { Count = count }); - } - - /// - /// Get api calls in date range. - /// - /// The name of the app. - /// The from date. - /// The to date. - /// - /// 200 => Usage tracking results returned. - /// 404 => App not found. - /// 400 => Range between from date and to date is not valid or has more than 100 days. - /// - [Authorize(Roles = SquidexRoles.AppEditor)] - [HttpGet] - [Route("apps/{app}/usages/{fromDate}/{toDate}")] - [ProducesResponseType(typeof(UsageDto[]), 200)] - public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) - { - if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) - { - return BadRequest(); - } - - var entities = await usageTracker.FindAsync(App.Id.ToString(), fromDate.Date, toDate.Date); - - var models = entities.Select(x => - { - var averageMs = x.TotalCount == 0 ? 0 : x.TotalElapsedMs / x.TotalCount; - - return new UsageDto { Date = x.Date, Count = x.TotalCount, AverageMs = averageMs }; - }).ToList(); - - return Ok(models); - } - } -} diff --git a/src/Squidex/Controllers/Api/Apps/AppController.cs b/src/Squidex/Controllers/Api/Apps/AppsController.cs similarity index 95% rename from src/Squidex/Controllers/Api/Apps/AppController.cs rename to src/Squidex/Controllers/Api/Apps/AppsController.cs index 4e6a90b51..b37f80f75 100644 --- a/src/Squidex/Controllers/Api/Apps/AppController.cs +++ b/src/Squidex/Controllers/Api/Apps/AppsController.cs @@ -1,5 +1,5 @@ // ========================================================================== -// AppController.cs +// AppsController.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -12,10 +12,10 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using NSwag.Annotations; +using Squidex.Controllers.Api.Apps.Models; using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Security; -using Squidex.Controllers.Api.Apps.Models; using Squidex.Pipeline; using Squidex.Read.Apps.Repositories; using Squidex.Write.Apps.Commands; @@ -28,11 +28,11 @@ namespace Squidex.Controllers.Api.Apps [Authorize] [ApiExceptionFilter] [SwaggerTag("Apps")] - public class AppController : ControllerBase + public class AppsController : ControllerBase { private readonly IAppRepository appRepository; - public AppController(ICommandBus commandBus, IAppRepository appRepository) + public AppsController(ICommandBus commandBus, IAppRepository appRepository) : base(commandBus) { this.appRepository = appRepository; diff --git a/src/Squidex/Controllers/Api/Assets/AssetController.cs b/src/Squidex/Controllers/Api/Assets/AssetsController.cs similarity index 98% rename from src/Squidex/Controllers/Api/Assets/AssetController.cs rename to src/Squidex/Controllers/Api/Assets/AssetsController.cs index 65f671612..99b822507 100644 --- a/src/Squidex/Controllers/Api/Assets/AssetController.cs +++ b/src/Squidex/Controllers/Api/Assets/AssetsController.cs @@ -1,5 +1,5 @@ // ========================================================================== -// AssetController.cs +// AssetsController.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -19,8 +19,8 @@ using NSwag.Annotations; using Squidex.Controllers.Api.Assets.Models; using Squidex.Core.Identity; using Squidex.Infrastructure; -using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Assets; +using Squidex.Infrastructure.CQRS.Commands; using Squidex.Infrastructure.Reflection; using Squidex.Pipeline; using Squidex.Read.Assets.Repositories; @@ -35,12 +35,12 @@ namespace Squidex.Controllers.Api.Assets [ApiExceptionFilter] [ServiceFilter(typeof(AppFilterAttribute))] [SwaggerTag("Assets")] - public class AssetController : ControllerBase + public class AssetsController : ControllerBase { private readonly IAssetRepository assetRepository; private readonly AssetConfig assetsConfig; - public AssetController( + public AssetsController( ICommandBus commandBus, IAssetRepository assetRepository, IOptions assetsConfig) diff --git a/src/Squidex/Controllers/Api/Apps/Models/UsageDto.cs b/src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs similarity index 87% rename from src/Squidex/Controllers/Api/Apps/Models/UsageDto.cs rename to src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs index 8e04538b5..17b0c632c 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/UsageDto.cs +++ b/src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs @@ -1,5 +1,5 @@ // ========================================================================== -// UsageDto.cs +// CallsUsageDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group @@ -8,9 +8,9 @@ using System; -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Controllers.Api.Statistics.Models { - public class UsageDto + public class CallsUsageDto { /// /// The date when the usage was tracked. diff --git a/src/Squidex/Controllers/Api/Apps/Models/MonthlyCallsDto.cs b/src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs similarity index 80% rename from src/Squidex/Controllers/Api/Apps/Models/MonthlyCallsDto.cs rename to src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs index 684011bb4..8fc66c5a7 100644 --- a/src/Squidex/Controllers/Api/Apps/Models/MonthlyCallsDto.cs +++ b/src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs @@ -1,14 +1,14 @@ // ========================================================================== -// MonthlyCallsDto.cs +// CurrentCallsDto.cs // Squidex Headless CMS // ========================================================================== // Copyright (c) Squidex Group // All rights reserved. // ========================================================================== -namespace Squidex.Controllers.Api.Apps.Models +namespace Squidex.Controllers.Api.Statistics.Models { - public class MonthlyCallsDto + public class CurrentCallsDto { /// /// The number of calls. diff --git a/src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs b/src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs new file mode 100644 index 000000000..4991d2506 --- /dev/null +++ b/src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs @@ -0,0 +1,18 @@ +// ========================================================================== +// CurrentStorageDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +namespace Squidex.Controllers.Api.Statistics.Models +{ + public class CurrentStorageDto + { + /// + /// The size in bytes. + /// + public long Size { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs b/src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs new file mode 100644 index 000000000..5da3a4f05 --- /dev/null +++ b/src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs @@ -0,0 +1,30 @@ +// ========================================================================== +// StorageUsageDto.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Controllers.Api.Statistics.Models +{ + public class StorageUsageDto + { + /// + /// The date when the usage was tracked. + /// + public DateTime Date { get; set; } + + /// + /// The number of assets. + /// + public long Count { get; set; } + + /// + /// The size in bytes. + /// + public long Size { get; set; } + } +} diff --git a/src/Squidex/Controllers/Api/Statistics/UsagesController.cs b/src/Squidex/Controllers/Api/Statistics/UsagesController.cs new file mode 100644 index 000000000..44c6f1569 --- /dev/null +++ b/src/Squidex/Controllers/Api/Statistics/UsagesController.cs @@ -0,0 +1,144 @@ +// ========================================================================== +// UsagesController.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Squidex.Controllers.Api.Statistics.Models; +using Squidex.Core.Identity; +using Squidex.Infrastructure.CQRS.Commands; +using Squidex.Infrastructure.UsageTracking; +using Squidex.Pipeline; +using Squidex.Read.Assets.Repositories; + +namespace Squidex.Controllers.Api.Statistics +{ + /// + /// Retrieves usage information for apps. + /// + [ApiExceptionFilter] + [ServiceFilter(typeof(AppFilterAttribute))] + [SwaggerTag("Statistics")] + public class UsagesController : ControllerBase + { + private readonly IUsageTracker usageTracker; + private readonly IAssetStatsRepository assetStatsRepository; + + public UsagesController(ICommandBus commandBus, IUsageTracker usageTracker, IAssetStatsRepository assetStatsRepository) + : base(commandBus) + { + this.usageTracker = usageTracker; + + this.assetStatsRepository = assetStatsRepository; + } + + /// + /// Get api calls for this month. + /// + /// The name of the app. + /// + /// 200 => Usage tracking results returned. + /// 404 => App not found. + /// + [Authorize(Roles = SquidexRoles.AppEditor)] + [HttpGet] + [Route("apps/{app}/usages/calls/month")] + [ProducesResponseType(typeof(CurrentCallsDto), 200)] + public async Task GetMonthlyCalls(string app) + { + var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today); + + return Ok(new CurrentCallsDto { Count = count }); + } + + /// + /// Get api calls in date range. + /// + /// The name of the app. + /// The from date. + /// The to date. + /// + /// 200 => API call returned. + /// 404 => App not found. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// + [Authorize(Roles = SquidexRoles.AppEditor)] + [HttpGet] + [Route("apps/{app}/usages/calls/{fromDate}/{toDate}")] + [ProducesResponseType(typeof(CallsUsageDto[]), 200)] + public async Task GetUsages(string app, DateTime fromDate, DateTime toDate) + { + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) + { + return BadRequest(); + } + + var entities = await usageTracker.QueryAsync(App.Id.ToString(), fromDate.Date, toDate.Date); + + var models = entities.Select(x => + { + var averageMs = x.TotalCount == 0 ? 0 : x.TotalElapsedMs / x.TotalCount; + + return new CallsUsageDto { Date = x.Date, Count = x.TotalCount, AverageMs = averageMs }; + }).ToList(); + + return Ok(models); + } + + /// + /// Get current size of all assets for today. + /// + /// The name of the app. + /// + /// 200 => Storage usage returned. + /// 404 => App not found. + /// + [Authorize(Roles = SquidexRoles.AppEditor)] + [HttpGet] + [Route("apps/{app}/usages/storage/today")] + [ProducesResponseType(typeof(CurrentStorageDto), 200)] + public async Task GetCurrentStorageSize(string app) + { + var size = await assetStatsRepository.GetTotalSizeAsync(App.Id); + + return Ok(new CurrentStorageDto { Size = size }); + } + + /// + /// Get storage usage in date range. + /// + /// The name of the app. + /// The from date. + /// The to date. + /// + /// 200 => Storage usage returned. + /// 404 => App not found. + /// 400 => Range between from date and to date is not valid or has more than 100 days. + /// + [Authorize(Roles = SquidexRoles.AppEditor)] + [HttpGet] + [Route("apps/{app}/usages/storage/{fromDate}/{toDate}")] + [ProducesResponseType(typeof(StorageUsageDto[]), 200)] + public async Task GetStorageSizes(string app, DateTime fromDate, DateTime toDate) + { + if (fromDate > toDate && (toDate - fromDate).TotalDays > 100) + { + return BadRequest(); + } + + var entities = await assetStatsRepository.QueryAsync(App.Id, fromDate.Date, toDate.Date); + + var models = entities.Select(x => new StorageUsageDto { Date = x.Date, Count = x.TotalCount, Size = x.TotalSize }).ToList(); + + return Ok(models); + } + } +} diff --git a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss index dbeb49f54..81a97db4e 100644 --- a/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss +++ b/src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss @@ -1,28 +1,22 @@ @import '_vars'; @import '_mixins'; -button { - display: inline-block; -} - -.col-right { - text-align: right; -} - -.faulted-icon { +.faulted { & { - @include transition(color .3s ease); - cursor: pointer; color: $color-theme-error; } - &:hover { - color: darken($color-theme-error, 10%); - } -} + &-icon { + & { + @include transition(color .3s ease); + cursor: pointer; + color: $color-theme-error; + } -.faulted { - color: $color-theme-error; + &:hover { + color: darken($color-theme-error, 10%); + } + } } .error-message { diff --git a/src/Squidex/app/features/administration/pages/users/users-page.component.scss b/src/Squidex/app/features/administration/pages/users/users-page.component.scss index d9fb3b3e7..47c1bd80f 100644 --- a/src/Squidex/app/features/administration/pages/users/users-page.component.scss +++ b/src/Squidex/app/features/administration/pages/users/users-page.component.scss @@ -1,14 +1,6 @@ @import '_vars'; @import '_mixins'; -.col-right { - text-align: right; -} - -.form-inline { - display: inline-block; -} - .user { &-name, &-email { diff --git a/src/Squidex/app/features/apps/pages/apps-page.component.scss b/src/Squidex/app/features/apps/pages/apps-page.component.scss index 97a82aff2..3aa95eef6 100644 --- a/src/Squidex/app/features/apps/pages/apps-page.component.scss +++ b/src/Squidex/app/features/apps/pages/apps-page.component.scss @@ -2,7 +2,7 @@ @import '_mixins'; .apps { - @include clearfix(); + @include clearfix; padding: 1.25rem; padding-left: $size-sidebar-width + .25rem; display: block; diff --git a/src/Squidex/app/features/assets/pages/assets-page.component.scss b/src/Squidex/app/features/assets/pages/assets-page.component.scss index be44e4482..68f6ea771 100644 --- a/src/Squidex/app/features/assets/pages/assets-page.component.scss +++ b/src/Squidex/app/features/assets/pages/assets-page.component.scss @@ -31,14 +31,6 @@ } } -.form-inline { - display: inline-block; -} - -.btn { - cursor: pointer; -} - .btn-input { width: 0; height: 0; diff --git a/src/Squidex/app/features/content/pages/contents/contents-page.component.scss b/src/Squidex/app/features/content/pages/contents/contents-page.component.scss index 20c393c1f..c1b2bf9f2 100644 --- a/src/Squidex/app/features/content/pages/contents/contents-page.component.scss +++ b/src/Squidex/app/features/content/pages/contents/contents-page.component.scss @@ -19,8 +19,4 @@ .form-control { width: 15rem; -} - -.form-inline { - display: inline-block; } \ No newline at end of file diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html index c935d2f6f..d2be98bb7 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.html @@ -4,7 +4,7 @@
-

Hi {{profileDisplayName}}

+

Hi {{profileDisplayName}}

Welcome to {{appName() | async}} dashboard. @@ -12,7 +12,7 @@
- +
@@ -22,7 +22,8 @@

A schema defines the structure of your content element.

- + +
@@ -32,7 +33,8 @@

Swagger compatible documentation for your schemas.

- + +
@@ -42,7 +44,8 @@

Provide feedback and request features to help us to improve Squidex.

- + +
@@ -53,24 +56,46 @@
-
+
+
+ +
+
+ +
+
+ +
+
+ +
- +
+
API calls this month
+
{{currentCalls}}
+
-
+ +
- +
-
-
API calls this month
-
{{monthlyCalls}}
+
+
Asset size today
+
{{currentStorage}}
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss index 67bbb5a54..3483dadf4 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss @@ -7,6 +7,11 @@ overflow-y: auto; } + &-title { + font-weight: light; + font-size: 1.4rem; + } + &-inner { padding: 2rem; padding-right: 1rem; @@ -14,9 +19,8 @@ } } -h1 { - font-weight: light; - font-size: 1.4rem; +:host /deep/ canvas { + height: 13rem !important; } .subtext { @@ -25,12 +29,6 @@ h1 { color: $color-subtext; } -a { - &.card { - cursor: pointer; - } -} - .card { & { margin-right: 1rem; @@ -39,25 +37,12 @@ a { float: left; } - &:hover { - @include box-shadow(0, 3px, 16px, .2px); - } - - &:hover, - &:active { - text-decoration: none; - } - - h4 { - color: $color-title; - } - - &-big { + &-lg { width: 33rem; } &-block { - min-height: 14.5rem; + min-height: 15.5rem; } &-image { @@ -71,24 +56,47 @@ a { } &-title { + color: $color-title; font-weight: light; font-size: 1.2rem; margin-top: .4rem; } + + &-link { + & { + cursor: pointer; + } + + &:hover { + @include box-shadow(0, 3px, 16px, .2px); + } + + &:focus { + outline: none; + } + + &:hover, + &:focus, + &:active { + text-decoration: none; + } + } } -.monthly-calls { +.aggregation { & { text-align: center; } - &-value { - font-size: 5rem; - } - &-label { color: $color-subtext; } + + &-value { + font-size: 3rem; + margin-top: 2rem; + margin-bottom: .5rem; + } } .app-name { diff --git a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts index 1e164a68c..061cfe871 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -14,6 +14,7 @@ import { AuthService, DateTime, fadeAnimation, + FileHelper, NotificationService, UsagesService } from 'shared'; @@ -33,11 +34,23 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit, public profileDisplayName = ''; - public chartCount: any; - public chartPerformance: any; - public chartOptions = { }; + public chartStorageCount: any; + public chartStorageSize: any; + public chartCallsCount: any; + public chartCallsPerformance: any; - public monthlyCalls: string | null = null; + public chartOptions = { + responsive: true, + scales: { + xAxes: [{ + display: true + }] + }, + maintainAspectRatio: false + }; + + public currentStorage: string | null = null; + public currentCalls: string | null = null; constructor(apps: AppsStoreService, notifications: NotificationService, private readonly auth: AuthService, @@ -52,7 +65,13 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit, public ngOnInit() { this.appName() - .switchMap(app => this.usagesService.getMonthlyCalls(app)) + .switchMap(app => this.usagesService.getTodayStorage(app)) + .subscribe(dto => { + this.currentStorage = FileHelper.fileSize(dto.size); + }); + + this.appName() + .switchMap(app => this.usagesService.getMonthCalls(app)) .subscribe(dto => { let count = dto.count; @@ -64,39 +83,67 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit, } else { count = Math.round(count); } - this.monthlyCalls = count + 'k'; + this.currentCalls = count + 'k'; } else { - this.monthlyCalls = count.toString(); + this.currentCalls = count.toString(); } }); this.appName() - .switchMap(app => this.usagesService.getUsages(app, DateTime.today().addDays(-30), DateTime.today())) + .switchMap(app => this.usagesService.getStorageUsages(app, DateTime.today().addDays(-30), DateTime.today())) .subscribe(dtos => { - const usages: any[] = dtos.map(x => { return { date: x.date.toStringFormat('L'), count: x.count, averageMs: x.averageMs }; }); + this.chartStorageCount = { + labels: createLabels(dtos), + datasets: [{ + label: 'Number of Assets', + lineTension: 0, + fill: false, + backgroundColor: 'rgba(61, 135, 213, 0.6)', + borderColor: 'rgba(61, 135, 213, 1)', + borderWidth: 1, + data: dtos.map(x => x.count) + }] + }; + + this.chartStorageSize = { + labels: createLabels(dtos), + datasets: [ { + label: 'Size of Assets (MB)', + lineTension: 0, + fill: false, + backgroundColor: 'rgba(61, 135, 213, 0.6)', + borderColor: 'rgba(61, 135, 213, 1)', + borderWidth: 1, + data: dtos.map(x => Math.round(10 * (x.size / (1024 * 1024))) / 10) + }] + }; + }); - this.chartCount = { - labels: usages.map(x => x.date), + this.appName() + .switchMap(app => this.usagesService.getCallsUsages(app, DateTime.today().addDays(-30), DateTime.today())) + .subscribe(dtos => { + this.chartCallsCount = { + labels: createLabels(dtos), datasets: [ { label: 'Number of API Calls', backgroundColor: 'rgba(61, 135, 213, 0.6)', borderColor: 'rgba(61, 135, 213, 1)', borderWidth: 1, - data: usages.map(x => x.count) + data: dtos.map(x => x.count) } ] }; - this.chartPerformance = { - labels: usages.map(x => x.date), + this.chartCallsPerformance = { + labels: createLabels(dtos), datasets: [ { label: 'API Performance (Milliseconds)', backgroundColor: 'rgba(61, 135, 213, 0.6)', borderColor: 'rgba(61, 135, 213, 1)', borderWidth: 1, - data: usages.map(x => x.averageMs) + data: dtos.map(x => x.averageMs) } ] }; @@ -117,3 +164,17 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit, } } +function createLabels(dtos: { date: DateTime }[]): string[] { + const labels: string[] = []; + + for (let dto of dtos) { + if (dto.date.weekDay === 1 || dto.date.weekDay === 4) { + labels.push(dto.date.toStringFormat('M-DD')); + } else { + labels.push(''); + } + } + + return labels; +} + diff --git a/src/Squidex/app/shared/services/usages.service.spec.ts b/src/Squidex/app/shared/services/usages.service.spec.ts index d5a6336fb..bbbeb0e96 100644 --- a/src/Squidex/app/shared/services/usages.service.spec.ts +++ b/src/Squidex/app/shared/services/usages.service.spec.ts @@ -12,9 +12,11 @@ import { IMock, Mock, Times } from 'typemoq'; import { ApiUrlConfig, AuthService, + CallsUsageDto, + CurrentCallsDto, + CurrentStorageDto, DateTime, - MonthlyCallsDto, - UsageDto, + StorageUsageDto, UsagesService } from './../'; @@ -27,14 +29,14 @@ describe('UsagesService', () => { usagesService = new UsagesService(authService.object, new ApiUrlConfig('http://service/p/')); }); - it('should make get request to get usages', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/usages/2017-10-12/2017-10-13')) + it('should make get request to get calls usages', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/usages/calls/2017-10-12/2017-10-13')) .returns(() => Observable.of( new Response( new ResponseOptions({ body: [{ date: '2017-10-12', - count: 1, + count: 10, averageMs: 130 }, { date: '2017-10-13', @@ -46,23 +48,23 @@ describe('UsagesService', () => { )) .verifiable(Times.once()); - let usages: UsageDto[] | null = null; + let usages: CallsUsageDto[] | null = null; - usagesService.getUsages('my-app', DateTime.parseISO_UTC('2017-10-12'), DateTime.parseISO_UTC('2017-10-13')).subscribe(result => { + usagesService.getCallsUsages('my-app', DateTime.parseISO_UTC('2017-10-12'), DateTime.parseISO_UTC('2017-10-13')).subscribe(result => { usages = result; }).unsubscribe(); expect(usages).toEqual( [ - new UsageDto(DateTime.parseISO_UTC('2017-10-12'), 1, 130), - new UsageDto(DateTime.parseISO_UTC('2017-10-13'), 13, 170) + new CallsUsageDto(DateTime.parseISO_UTC('2017-10-12'), 10, 130), + new CallsUsageDto(DateTime.parseISO_UTC('2017-10-13'), 13, 170) ]); authService.verifyAll(); }); - it('should make get request to get monthly calls', () => { - authService.setup(x => x.authGet('http://service/p/api/apps/my-app/usages/monthly')) + it('should make get request to get month calls', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/usages/calls/month')) .returns(() => Observable.of( new Response( new ResponseOptions({ @@ -72,13 +74,69 @@ describe('UsagesService', () => { )) .verifiable(Times.once()); - let usages: MonthlyCallsDto | null = null; + let usages: CurrentCallsDto | null = null; - usagesService.getMonthlyCalls('my-app').subscribe(result => { + usagesService.getMonthCalls('my-app').subscribe(result => { usages = result; }).unsubscribe(); - expect(usages).toEqual(new MonthlyCallsDto(130)); + expect(usages).toEqual(new CurrentCallsDto(130)); + + authService.verifyAll(); + }); + + it('should make get request to get storage usages', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/usages/storage/2017-10-12/2017-10-13')) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: [{ + date: '2017-10-12', + count: 10, + size: 130 + }, { + date: '2017-10-13', + count: 13, + size: 170 + }] + }) + ) + )) + .verifiable(Times.once()); + + let usages: StorageUsageDto[] | null = null; + + usagesService.getStorageUsages('my-app', DateTime.parseISO_UTC('2017-10-12'), DateTime.parseISO_UTC('2017-10-13')).subscribe(result => { + usages = result; + }).unsubscribe(); + + expect(usages).toEqual( + [ + new StorageUsageDto(DateTime.parseISO_UTC('2017-10-12'), 10, 130), + new StorageUsageDto(DateTime.parseISO_UTC('2017-10-13'), 13, 170) + ]); + + authService.verifyAll(); + }); + + it('should make get request to get today storage', () => { + authService.setup(x => x.authGet('http://service/p/api/apps/my-app/usages/storage/today')) + .returns(() => Observable.of( + new Response( + new ResponseOptions({ + body: { size: 130 } + }) + ) + )) + .verifiable(Times.once()); + + let usages: CurrentStorageDto | null = null; + + usagesService.getTodayStorage('my-app').subscribe(result => { + usages = result; + }).unsubscribe(); + + expect(usages).toEqual(new CurrentStorageDto(130)); authService.verifyAll(); }); diff --git a/src/Squidex/app/shared/services/usages.service.ts b/src/Squidex/app/shared/services/usages.service.ts index 8c15a2a37..0bc20fc29 100644 --- a/src/Squidex/app/shared/services/usages.service.ts +++ b/src/Squidex/app/shared/services/usages.service.ts @@ -13,7 +13,7 @@ import 'framework/angular/http-extensions'; import { ApiUrlConfig, DateTime } from 'framework'; import { AuthService } from './auth.service'; -export class UsageDto { +export class CallsUsageDto { constructor( public readonly date: DateTime, public readonly count: number, @@ -22,7 +22,23 @@ export class UsageDto { } } -export class MonthlyCallsDto { +export class StorageUsageDto { + constructor( + public readonly date: DateTime, + public readonly count: number, + public readonly size: number + ) { + } +} + +export class CurrentStorageDto { + constructor( + public readonly size: number + ) { + } +} + +export class CurrentCallsDto { constructor( public readonly count: number ) { @@ -37,19 +53,44 @@ export class UsagesService { ) { } - public getMonthlyCalls(app: string): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/monthly`); + public getMonthCalls(app: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/month`); + + return this.authService.authGet(url) + .map(response => response.json()) + .map(response => new CurrentCallsDto(response.count)) + .catchError('Failed to load monthly api calls. Please reload.'); + } + + public getCallsUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/calls/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`); return this.authService.authGet(url) .map(response => response.json()) .map(response => { - return new MonthlyCallsDto(response.count); + const items: any[] = response; + + return items.map(item => { + return new CallsUsageDto( + DateTime.parseISO_UTC(item.date), + item.count, + item.averageMs); + }); }) - .catchError('Failed to load monthly calls. Please reload.'); + .catchError('Failed to load calls usage. Please reload.'); } - public getUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable { - const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`); + public getTodayStorage(app: string): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/today`); + + return this.authService.authGet(url) + .map(response => response.json()) + .map(response => new CurrentStorageDto(response.size)) + .catchError('Failed to load todays storage size. Please reload.'); + } + + public getStorageUsages(app: string, fromDate: DateTime, toDate: DateTime): Observable { + const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/storage/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`); return this.authService.authGet(url) .map(response => response.json()) @@ -57,12 +98,12 @@ export class UsagesService { const items: any[] = response; return items.map(item => { - return new UsageDto( + return new StorageUsageDto( DateTime.parseISO_UTC(item.date), item.count, - item.averageMs); + item.size); }); }) - .catchError('Failed to load usage. Please reload.'); + .catchError('Failed to load storage usage. Please reload.'); } } \ No newline at end of file diff --git a/src/Squidex/app/theme/_bootstrap.scss b/src/Squidex/app/theme/_bootstrap.scss index 42ccba586..d1590ac54 100644 --- a/src/Squidex/app/theme/_bootstrap.scss +++ b/src/Squidex/app/theme/_bootstrap.scss @@ -5,6 +5,10 @@ body { background: $color-background; } +.col-right { + text-align: right; +} + .navbar { & { height: $size-navbar-height; diff --git a/src/Squidex/app/theme/_panels.scss b/src/Squidex/app/theme/_panels.scss index 65cd9f34e..ae513eec6 100644 --- a/src/Squidex/app/theme/_panels.scss +++ b/src/Squidex/app/theme/_panels.scss @@ -51,6 +51,10 @@ margin-top: 1.2rem; position: relative; } + + .form-inline { + display: inline-block; + } } &-main { diff --git a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs index 07b96411f..b0a233400 100644 --- a/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs +++ b/tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs @@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.UsageTracking { sut.Dispose(); - return Assert.ThrowsAsync(() => sut.FindAsync("key1", DateTime.Today, DateTime.Today.AddDays(1))); + return Assert.ThrowsAsync(() => sut.QueryAsync("key1", DateTime.Today, DateTime.Today.AddDays(1))); } [Fact] @@ -65,7 +65,7 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage(date.AddDays(7), 17, 22) }; - usageStore.Setup(x => x.FindAsync("key", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))).Returns(Task.FromResult(originalData)); + usageStore.Setup(x => x.QueryAsync("key", new DateTime(2016, 1, 1), new DateTime(2016, 1, 31))).Returns(Task.FromResult(originalData)); var result = await sut.GetMonthlyCalls("key", date); @@ -86,9 +86,9 @@ namespace Squidex.Infrastructure.UsageTracking new StoredUsage(dateFrom.AddDays(7), 17, 22) }; - usageStore.Setup(x => x.FindAsync("key", dateFrom, dateTo)).Returns(Task.FromResult(originalData)); + usageStore.Setup(x => x.QueryAsync("key", dateFrom, dateTo)).Returns(Task.FromResult(originalData)); - var result = await sut.FindAsync("key", dateFrom, dateTo); + var result = await sut.QueryAsync("key", dateFrom, dateTo); result.ShouldBeEquivalentTo(new List {