diff --git a/src/Squidex.Events/Assets/AssetDeleted.cs b/src/Squidex.Events/Assets/AssetDeleted.cs index 6299e5ce4..6a6d47c77 100644 --- a/src/Squidex.Events/Assets/AssetDeleted.cs +++ b/src/Squidex.Events/Assets/AssetDeleted.cs @@ -13,5 +13,6 @@ namespace Squidex.Events.Assets [TypeName("AssetDeletedEvent")] public sealed class AssetDeleted : AssetEvent { + public long DeletedSize { get; set; } } } diff --git a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs index 8f1baa41d..166fbd0cd 100644 --- a/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs +++ b/src/Squidex.Infrastructure/UsageTracking/BackgroundUsageTracker.cs @@ -113,9 +113,11 @@ namespace Squidex.Infrastructure.UsageTracking var originalUsages = await usageStore.FindAsync(key, fromDate, toDate); var enrichedUsages = new List(); + var usagesDictionary = originalUsages.ToDictionary(x => x.Date); + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) { - enrichedUsages.Add(originalUsages.FirstOrDefault(x => x.Date == date) ?? new StoredUsage(date, 0, 0)); + enrichedUsages.Add(usagesDictionary.GetOrDefault(date) ?? new StoredUsage(date, 0, 0)); } return enrichedUsages; diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsEntity.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsEntity.cs new file mode 100644 index 000000000..911e9ae6a --- /dev/null +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsEntity.cs @@ -0,0 +1,40 @@ +// ========================================================================== +// MongoAssetStatsEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Squidex.Read.Assets; + +namespace Squidex.Read.MongoDb.Assets +{ + public sealed class MongoAssetStatsEntity : IAssetStatsEntity + { + [BsonId] + [BsonElement] + [BsonRepresentation(BsonType.String)] + public string Id { get; set; } + + [BsonRequired] + [BsonElement] + [BsonDateTimeOptions(DateOnly = true)] + public DateTime Date { get; set; } + + [BsonRequired] + [BsonElement] + public Guid AppId { get; set; } + + [BsonRequired] + [BsonElement] + public long TotalSize { get; set; } + + [BsonRequired] + [BsonElement] + public long TotalCount { get; set; } + } +} diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsRepository.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsRepository.cs new file mode 100644 index 000000000..7db5a3056 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsRepository.cs @@ -0,0 +1,91 @@ +// ========================================================================== +// MongoAssetStatsRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Infrastructure; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.MongoDb; +using Squidex.Read.Assets; +using Squidex.Read.Assets.Repositories; + +namespace Squidex.Read.MongoDb.Assets +{ + public partial class MongoAssetStatsRepository : MongoRepositoryBase, IAssetStatsRepository, IEventConsumer + { + public MongoAssetStatsRepository(IMongoDatabase database) + : base(database) + { + } + + protected override string CollectionName() + { + return "Projections_AssetStats"; + } + + protected override Task SetupCollectionAsync(IMongoCollection collection) + { + return Task.WhenAll( + collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.AppId).Ascending(x => x.Date)), + collection.Indexes.CreateOneAsync(IndexKeys.Ascending(x => x.AppId).Descending(x => x.Date))); + } + + public async Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate) + { + var originalSizes = await Collection.Find(x => x.AppId == appId && x.Date >= fromDate && x.Date <= toDate).SortBy(x => x.Date).ToListAsync(); + var enrichedSizes = new List(); + + var sizesDictionary = originalSizes.ToDictionary(x => x.Date); + + var previousSize = long.MinValue; + var previousCount = long.MinValue; + + for (var date = fromDate; date <= toDate; date = date.AddDays(1)) + { + var size = sizesDictionary.GetOrDefault(date); + + if (size != null) + { + previousSize = size.TotalSize; + previousCount = size.TotalCount; + } + else + { + if (previousSize < 0) + { + var firstBeforeRange = await Collection.Find(x => x.AppId == appId && x.Date < fromDate).SortByDescending(x => x.Date).FirstOrDefaultAsync(); + + previousSize = firstBeforeRange?.TotalSize ?? 0L; + previousCount = firstBeforeRange?.TotalCount ?? 0L; + } + + size = new MongoAssetStatsEntity + { + Date = date, + TotalSize = previousSize, + TotalCount = previousCount + }; + } + + enrichedSizes.Add(size); + } + + return enrichedSizes; + } + + public async Task GetTotalSizeAsync(Guid appId) + { + var entity = await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Date).FirstOrDefaultAsync(); + + return entity?.TotalSize ?? 0; + } + } +} diff --git a/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs b/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs new file mode 100644 index 000000000..f0430b850 --- /dev/null +++ b/src/Squidex.Read.MongoDb/Assets/MongoAssetStatsRepository_EventHandling.cs @@ -0,0 +1,78 @@ +// ========================================================================== +// MongoAssetStatsRepository_EventHandling.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Threading.Tasks; +using MongoDB.Driver; +using Squidex.Events.Assets; +using Squidex.Infrastructure.CQRS.Events; +using Squidex.Infrastructure.Dispatching; + +namespace Squidex.Read.MongoDb.Assets +{ + public partial class MongoAssetStatsRepository + { + private static readonly UpdateOptions Upsert = new UpdateOptions { IsUpsert = true }; + + public string Name + { + get { return GetType().Name; } + } + + public string EventsFilter + { + get { return "^asset-"; } + } + + public Task On(Envelope @event) + { + return this.DispatchActionAsync(@event.Payload, @event.Headers); + } + + protected Task On(AssetCreated @event, EnvelopeHeaders headers) + { + return UpdateSizeAsync(@event.AppId.Id, headers.Timestamp().ToDateTimeUtc().Date, @event.FileSize, 1); + } + + protected Task On(AssetUpdated @event, EnvelopeHeaders headers) + { + return UpdateSizeAsync(@event.AppId.Id, headers.Timestamp().ToDateTimeUtc().Date, @event.FileSize, 0); + } + + protected Task On(AssetDeleted @event, EnvelopeHeaders headers) + { + return UpdateSizeAsync(@event.AppId.Id, headers.Timestamp().ToDateTimeUtc().Date, -@event.DeletedSize, -1); + } + + private async Task UpdateSizeAsync(Guid appId, DateTime date, long size, long count) + { + var id = $"{appId}_{date:yyyy-MM-dd}"; + + var entity = await Collection.Find(x => x.Id == id).FirstOrDefaultAsync(); + + if (entity == null) + { + var last = await Collection.Find(x => x.AppId == appId).SortByDescending(x => x.Date).FirstOrDefaultAsync(); + + entity = new MongoAssetStatsEntity + { + Id = id, + Date = date, + AppId = appId, + TotalSize = last?.TotalSize ?? 0, + TotalCount = last?.TotalCount ?? 0 + }; + } + + entity.TotalSize += size; + entity.TotalCount += count; + + await Collection.ReplaceOneAsync(x => x.Id == id, entity, Upsert); + } + } +} diff --git a/src/Squidex.Read/Assets/IAssetStatsEntity.cs b/src/Squidex.Read/Assets/IAssetStatsEntity.cs new file mode 100644 index 000000000..3f0887ff1 --- /dev/null +++ b/src/Squidex.Read/Assets/IAssetStatsEntity.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IAssetDaySizeEntity.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; + +namespace Squidex.Read.Assets +{ + public interface IAssetStatsEntity + { + DateTime Date { get; } + + long TotalSize { get; } + + long TotalCount { get; } + } +} diff --git a/src/Squidex.Read/Assets/Repositories/IAssetStatsRepository.cs b/src/Squidex.Read/Assets/Repositories/IAssetStatsRepository.cs new file mode 100644 index 000000000..66acc5e9d --- /dev/null +++ b/src/Squidex.Read/Assets/Repositories/IAssetStatsRepository.cs @@ -0,0 +1,21 @@ +// ========================================================================== +// IAssetDaySizeRepository.cs +// Squidex Headless CMS +// ========================================================================== +// Copyright (c) Squidex Group +// All rights reserved. +// ========================================================================== + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Squidex.Read.Assets.Repositories +{ + public interface IAssetStatsRepository + { + Task> QueryAsync(Guid appId, DateTime fromDate, DateTime toDate); + + Task GetTotalSizeAsync(Guid appId); + } +} diff --git a/src/Squidex.Write/Assets/AssetDomainObject.cs b/src/Squidex.Write/Assets/AssetDomainObject.cs index 700e1d878..a3a7a375b 100644 --- a/src/Squidex.Write/Assets/AssetDomainObject.cs +++ b/src/Squidex.Write/Assets/AssetDomainObject.cs @@ -23,6 +23,7 @@ namespace Squidex.Write.Assets { private bool isDeleted; private long fileVersion = -1; + private long totalSize; private string fileName; public bool IsDeleted @@ -49,11 +50,15 @@ namespace Squidex.Write.Assets { fileVersion = @event.FileVersion; fileName = @event.FileName; + + totalSize += @event.FileSize; } protected void On(AssetUpdated @event) { fileVersion = @event.FileVersion; + + totalSize += @event.FileSize; } protected void On(AssetRenamed @event) @@ -115,7 +120,7 @@ namespace Squidex.Write.Assets VerifyCreatedAndNotDeleted(); - RaiseEvent(SimpleMapper.Map(command, new AssetDeleted())); + RaiseEvent(SimpleMapper.Map(command, new AssetDeleted { DeletedSize = totalSize })); return this; } diff --git a/src/Squidex/Config/Domain/StoreMongoDbModule.cs b/src/Squidex/Config/Domain/StoreMongoDbModule.cs index 68c8c5964..8ff868297 100644 --- a/src/Squidex/Config/Domain/StoreMongoDbModule.cs +++ b/src/Squidex/Config/Domain/StoreMongoDbModule.cs @@ -142,6 +142,14 @@ namespace Squidex.Config.Domain .AsSelf() .SingleInstance(); + builder.RegisterType() + .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) + .As() + .As() + .As() + .AsSelf() + .SingleInstance(); + builder.RegisterType() .WithParameter(ResolvedParameter.ForNamed(MongoDatabaseRegistration)) .As() 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 6dd0346f6..b73f0f975 100644 --- a/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts +++ b/src/Squidex/app/features/dashboard/pages/dashboard-page.component.ts @@ -54,10 +54,19 @@ export class DashboardPageComponent extends AppComponentBase implements OnInit, this.appName() .switchMap(app => this.usagesService.getMonthlyCalls(app)) .subscribe(dto => { - if (dto.count > 1000) { - this.monthlyCalls = Math.round(dto.count / 1000) + 'k'; + let count = dto.count; + + if (count > 1000) { + count = count / 1000; + + if (count < 10) { + count = Math.round(count * 10) / 10; + } else { + count = Math.round(count); + } + this.monthlyCalls = count + 'k'; } else { - this.monthlyCalls = dto.count.toString(); + this.monthlyCalls = count.toString(); } }); diff --git a/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs b/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs index 7319fbee0..f0f12aa01 100644 --- a/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs +++ b/tests/Squidex.Write.Tests/Assets/AssetDomainObjectTests.cs @@ -187,6 +187,7 @@ namespace Squidex.Write.Assets public void Delete_should_update_properties_create_events() { CreateAsset(); + UpdateAsset(); sut.Delete(CreateAssetCommand(new DeleteAsset())); @@ -194,7 +195,7 @@ namespace Squidex.Write.Assets sut.GetUncomittedEvents() .ShouldHaveSameEvents( - CreateAssetEvent(new AssetDeleted()) + CreateAssetEvent(new AssetDeleted { DeletedSize = 2048 }) ); } @@ -205,6 +206,13 @@ namespace Squidex.Write.Assets ((IAggregate)sut).ClearUncommittedEvents(); } + private void UpdateAsset() + { + sut.Update(CreateAssetCommand(new UpdateAsset { File = file })); + + ((IAggregate)sut).ClearUncommittedEvents(); + } + private void DeleteAsset() { sut.Delete(CreateAssetCommand(new DeleteAsset()));