Browse Source

Asset storage statistics.

pull/65/head
Sebastian Stehle 9 years ago
parent
commit
b37efec39e
  1. 2
      src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs
  2. 6
      src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs
  3. 2
      src/Squidex.Infrastructure/UsageTracking/IUsageStore.cs
  4. 2
      src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs
  5. 2
      src/Squidex/.sass-lint.yml
  6. 92
      src/Squidex/Controllers/Api/Apps/AppUsageController.cs
  7. 8
      src/Squidex/Controllers/Api/Apps/AppsController.cs
  8. 8
      src/Squidex/Controllers/Api/Assets/AssetsController.cs
  9. 6
      src/Squidex/Controllers/Api/Statistics/Models/CallsUsageDto.cs
  10. 6
      src/Squidex/Controllers/Api/Statistics/Models/CurrentCallsDto.cs
  11. 18
      src/Squidex/Controllers/Api/Statistics/Models/CurrentStorageDto.cs
  12. 30
      src/Squidex/Controllers/Api/Statistics/Models/StorageUsageDto.cs
  13. 144
      src/Squidex/Controllers/Api/Statistics/UsagesController.cs
  14. 28
      src/Squidex/app/features/administration/pages/event-consumers/event-consumers-page.component.scss
  15. 8
      src/Squidex/app/features/administration/pages/users/users-page.component.scss
  16. 2
      src/Squidex/app/features/apps/pages/apps-page.component.scss
  17. 8
      src/Squidex/app/features/assets/pages/assets-page.component.scss
  18. 4
      src/Squidex/app/features/content/pages/contents/contents-page.component.scss
  19. 49
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.html
  20. 66
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.scss
  21. 91
      src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts
  22. 86
      src/Squidex/app/shared/services/usages.service.spec.ts
  23. 63
      src/Squidex/app/shared/services/usages.service.ts
  24. 4
      src/Squidex/app/theme/_bootstrap.scss
  25. 4
      src/Squidex/app/theme/_panels.scss
  26. 8
      tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

2
src/Squidex.Infrastructure.MongoDb/UsageTracker/MongoUsageStore.cs

@ -48,7 +48,7 @@ namespace Squidex.Infrastructure.MongoDb.UsageTracker
Upsert);
}
public async Task<IReadOnlyList<StoredUsage>> FindAsync(string key, DateTime fromDate, DateTime toDate)
public async Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate)
{
var entities = await Collection.Find(x => x.Key == key && x.Date >= fromDate && x.Date <= toDate).ToListAsync();

6
src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs

@ -104,13 +104,13 @@ namespace Squidex.Infrastructure.UsageTracking
return TaskHelper.Done;
}
public async Task<IReadOnlyList<StoredUsage>> FindAsync(string key, DateTime fromDate, DateTime toDate)
public async Task<IReadOnlyList<StoredUsage>> 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<StoredUsage>();
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);
}

2
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<IReadOnlyList<StoredUsage>> FindAsync(string key, DateTime fromDate, DateTime toDate);
Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate);
}
}

2
src/Squidex.Infrastructure/UsageTracking/IUsageTracker.cs

@ -18,6 +18,6 @@ namespace Squidex.Infrastructure.UsageTracking
Task<long> GetMonthlyCalls(string key, DateTime date);
Task<IReadOnlyList<StoredUsage>> FindAsync(string key, DateTime fromDate, DateTime toDate);
Task<IReadOnlyList<StoredUsage>> QueryAsync(string key, DateTime fromDate, DateTime toDate);
}
}

2
src/Squidex/.sass-lint.yml

@ -1,5 +1,7 @@
rules:
no-ids:
- 1
no-important:
- 0
final-newline:
- 0

92
src/Squidex/Controllers/Api/Apps/AppUsageController.cs

@ -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
{
/// <summary>
/// Retrieves usage information for apps.
/// </summary>
[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;
}
/// <summary>
/// Get api calls for this month.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Usage tracking results returned.
/// 404 => App not found.
/// </returns>
[Authorize(Roles = SquidexRoles.AppEditor)]
[HttpGet]
[Route("apps/{app}/usages/monthly")]
[ProducesResponseType(typeof(MonthlyCallsDto), 200)]
public async Task<IActionResult> GetMonthlyCalls(string app)
{
var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today);
return Ok(new MonthlyCallsDto { Count = count });
}
/// <summary>
/// Get api calls in date range.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="fromDate">The from date.</param>
/// <param name="toDate">The to date.</param>
/// <returns>
/// 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.
/// </returns>
[Authorize(Roles = SquidexRoles.AppEditor)]
[HttpGet]
[Route("apps/{app}/usages/{fromDate}/{toDate}")]
[ProducesResponseType(typeof(UsageDto[]), 200)]
public async Task<IActionResult> 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);
}
}
}

8
src/Squidex/Controllers/Api/Apps/AppController.cs → 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;

8
src/Squidex/Controllers/Api/Assets/AssetController.cs → 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<AssetConfig> assetsConfig)

6
src/Squidex/Controllers/Api/Apps/Models/UsageDto.cs → 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
{
/// <summary>
/// The date when the usage was tracked.

6
src/Squidex/Controllers/Api/Apps/Models/MonthlyCallsDto.cs → 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
{
/// <summary>
/// The number of calls.

18
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
{
/// <summary>
/// The size in bytes.
/// </summary>
public long Size { get; set; }
}
}

30
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
{
/// <summary>
/// The date when the usage was tracked.
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// The number of assets.
/// </summary>
public long Count { get; set; }
/// <summary>
/// The size in bytes.
/// </summary>
public long Size { get; set; }
}
}

144
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
{
/// <summary>
/// Retrieves usage information for apps.
/// </summary>
[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;
}
/// <summary>
/// Get api calls for this month.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Usage tracking results returned.
/// 404 => App not found.
/// </returns>
[Authorize(Roles = SquidexRoles.AppEditor)]
[HttpGet]
[Route("apps/{app}/usages/calls/month")]
[ProducesResponseType(typeof(CurrentCallsDto), 200)]
public async Task<IActionResult> GetMonthlyCalls(string app)
{
var count = await usageTracker.GetMonthlyCalls(App.Id.ToString(), DateTime.Today);
return Ok(new CurrentCallsDto { Count = count });
}
/// <summary>
/// Get api calls in date range.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="fromDate">The from date.</param>
/// <param name="toDate">The to date.</param>
/// <returns>
/// 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.
/// </returns>
[Authorize(Roles = SquidexRoles.AppEditor)]
[HttpGet]
[Route("apps/{app}/usages/calls/{fromDate}/{toDate}")]
[ProducesResponseType(typeof(CallsUsageDto[]), 200)]
public async Task<IActionResult> 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);
}
/// <summary>
/// Get current size of all assets for today.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <returns>
/// 200 => Storage usage returned.
/// 404 => App not found.
/// </returns>
[Authorize(Roles = SquidexRoles.AppEditor)]
[HttpGet]
[Route("apps/{app}/usages/storage/today")]
[ProducesResponseType(typeof(CurrentStorageDto), 200)]
public async Task<IActionResult> GetCurrentStorageSize(string app)
{
var size = await assetStatsRepository.GetTotalSizeAsync(App.Id);
return Ok(new CurrentStorageDto { Size = size });
}
/// <summary>
/// Get storage usage in date range.
/// </summary>
/// <param name="app">The name of the app.</param>
/// <param name="fromDate">The from date.</param>
/// <param name="toDate">The to date.</param>
/// <returns>
/// 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.
/// </returns>
[Authorize(Roles = SquidexRoles.AppEditor)]
[HttpGet]
[Route("apps/{app}/usages/storage/{fromDate}/{toDate}")]
[ProducesResponseType(typeof(StorageUsageDto[]), 200)]
public async Task<IActionResult> 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);
}
}
}

28
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 {

8
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 {

2
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;

8
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;

4
src/Squidex/app/features/content/pages/contents/contents-page.component.scss

@ -19,8 +19,4 @@
.form-control {
width: 15rem;
}
.form-inline {
display: inline-block;
}

49
src/Squidex/app/features/dashboard/pages/dashboard-page.component.html

@ -4,7 +4,7 @@
<div class="dashboard-inner">
<div>
<h1>Hi {{profileDisplayName}}</h1>
<h1 class="dashboard-title">Hi {{profileDisplayName}}</h1>
<div class="subtext">
Welcome to <span class="app-name">{{appName() | async}}</span> dashboard.
@ -12,7 +12,7 @@
</div>
<div class="clearfix">
<a class="card" [routerLink]="['schemas', { showDialog: true }]">
<a class="card card-link" [routerLink]="['schemas', { showDialog: true }]">
<div class="card-block">
<div class="card-image">
<img src="/images/dashboard-schema.png" />
@ -22,7 +22,8 @@
<p class="card-text">A schema defines the structure of your content element.</p>
</div>
</a>
<a class="card" href="/api/content/{{appName() | async}}/docs" target="_blank">
<a class="card card-link" href="/api/content/{{appName() | async}}/docs" target="_blank">
<div class="card-block">
<div class="card-image">
<img src="/images/dashboard-api.png" />
@ -32,7 +33,8 @@
<p class="card-text">Swagger compatible documentation for your schemas.</p>
</div>
</a>
<a class="card" (click)="showForum()">
<a class="card card-link" (click)="showForum()">
<div class="card-block">
<div class="card-image">
<img src="/images/dashboard-feedback.png" />
@ -42,7 +44,8 @@
<p class="card-text">Provide feedback and request features to help us to improve Squidex.</p>
</div>
</a>
<a class="card" href="https://github.com/squidex/squidex" target="_blank">
<a class="card card-link" href="https://github.com/squidex/squidex" target="_blank">
<div class="card-block">
<div class="card-image">
<img src="/images/dashboard-github.png" />
@ -53,24 +56,46 @@
</div>
</a>
<div class="card card-big">
<div class="card card-lg">
<div class="card-block">
<chart type="bar" [data]="chartCallsCount" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card-lg">
<div class="card-block">
<chart type="bar" [data]="chartCallsPerformance" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card">
<div class="card-block">
<chart type="bar" [data]="chartCount" [options]="chartOptions"></chart>
<div class="aggregation" *ngIf="currentCalls">
<div class="aggregation-label">API calls this month</div>
<div class="aggregation-value">{{currentCalls}}</div>
</div>
</div>
</div>
<div class="card card-big">
<div class="card card-lg">
<div class="card-block">
<chart type="bar" [data]="chartPerformance" [options]="chartOptions"></chart>
<chart type="line" [data]="chartStorageCount" [options]="chartOptions"></chart>
</div>
</div>
<div class="card card">
<div class="card-block">
<div class="monthly-calls" *ngIf="monthlyCalls">
<div class="monthly-calls-label">API calls this month</div>
<div class="monthly-calls-value">{{monthlyCalls}}</div>
<div class="aggregation" *ngIf="currentStorage">
<div class="aggregation-label">Asset size today</div>
<div class="aggregation-value">{{currentStorage}}</div>
</div>
</div>
</div>
<div class="card card-lg">
<div class="card-block">
<chart type="line" [data]="chartStorageSize" [options]="chartOptions"></chart>
</div>
</div>
</div>
</div>

66
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 {

91
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;
}

86
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();
});

63
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<MonthlyCallsDto> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/monthly`);
public getMonthCalls(app: string): Observable<CurrentCallsDto> {
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<CallsUsageDto[]> {
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<UsageDto[]> {
const url = this.apiUrl.buildUrl(`api/apps/${app}/usages/${fromDate.toStringFormat('YYYY-MM-DD')}/${toDate.toStringFormat('YYYY-MM-DD')}`);
public getTodayStorage(app: string): Observable<CurrentStorageDto> {
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<StorageUsageDto[]> {
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.');
}
}

4
src/Squidex/app/theme/_bootstrap.scss

@ -5,6 +5,10 @@ body {
background: $color-background;
}
.col-right {
text-align: right;
}
.navbar {
& {
height: $size-navbar-height;

4
src/Squidex/app/theme/_panels.scss

@ -51,6 +51,10 @@
margin-top: 1.2rem;
position: relative;
}
.form-inline {
display: inline-block;
}
}
&-main {

8
tests/Squidex.Infrastructure.Tests/UsageTracking/BackgroundUsageTrackerTests.cs

@ -41,7 +41,7 @@ namespace Squidex.Infrastructure.UsageTracking
{
sut.Dispose();
return Assert.ThrowsAsync<ObjectDisposedException>(() => sut.FindAsync("key1", DateTime.Today, DateTime.Today.AddDays(1)));
return Assert.ThrowsAsync<ObjectDisposedException>(() => 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<StoredUsage>
{

Loading…
Cancel
Save