From 2fae67e43687c3aa28d60097c726d20c0cab0bdc Mon Sep 17 00:00:00 2001 From: Daniel Silva Date: Thu, 9 Nov 2023 10:00:40 +0100 Subject: [PATCH] Show current quota usage on Identity View (#367) * feat: progress bar and hardcoded usage values * feat: make API return actual quota usage * fix: tests missing new DI factory * fix: bad quota tag * refactor: rename var * refactor: remove needless line * refactor: move metricCalculatorFactoryfake creation to CreateHandler * refactor: simplify Handler with GetIdentityResponse.Create * refactor: move extraction of quota attributes to QuotaDTO ctor * refactor: rename mock/stub. Use Dummy instead of Fake * Trigger Build * refactor: create overload for CreateHandler This way we abstract the creation of optional dummies --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../AdminUi/ClientApp/src/app/app.module.ts | 6 ++- .../identity-edit/identity-edit.component.css | 9 ++++ .../identity-edit.component.html | 9 ++-- .../services/quotas-service/quotas.service.ts | 1 + .../src/Quotas.Application/DTOs/QuotaDTO.cs | 12 +++-- .../GetIdentity/GetIdentityResponse.cs | 50 ++++++++++--------- .../Identities/Queries/GetIdentity/Handler.cs | 8 +-- .../Identities/GetIdentity/HandlerTests.cs | 25 ++++++---- 8 files changed, 75 insertions(+), 45 deletions(-) diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/app.module.ts b/AdminUi/src/AdminUi/ClientApp/src/app/app.module.ts index 0e149672e9..bd469b4fca 100644 --- a/AdminUi/src/AdminUi/ClientApp/src/app/app.module.ts +++ b/AdminUi/src/AdminUi/ClientApp/src/app/app.module.ts @@ -23,6 +23,7 @@ import { MatIconModule } from "@angular/material/icon"; import { MatInputModule } from "@angular/material/input"; import { MatListModule } from "@angular/material/list"; import { MatPaginatorModule } from "@angular/material/paginator"; +import { MatProgressBarModule } from "@angular/material/progress-bar"; import { MatProgressSpinnerModule } from "@angular/material/progress-spinner"; import { MatSelectModule } from "@angular/material/select"; import { MatSidenavModule } from "@angular/material/sidenav"; @@ -46,6 +47,7 @@ import { IdentityListComponent } from "./components/quotas/identity/identity-lis import { TierEditComponent } from "./components/quotas/tier/tier-edit/tier-edit.component"; import { TierListComponent } from "./components/quotas/tier/tier-list/tier-list.component"; import { ConfirmationDialogComponent } from "./components/shared/confirmation-dialog/confirmation-dialog.component"; +import { IdentitiesOverviewComponent } from "./components/shared/identities-overview/identities-overview.component"; import { LoginComponent } from "./components/shared/login/login.component"; import { SidebarComponent } from "./components/sidebar/sidebar.component"; import { TopbarComponent } from "./components/topbar/topbar.component"; @@ -54,7 +56,6 @@ import { LoggerWriterService } from "./services/logger-writer-service/logger-wri import { SidebarService } from "./services/sidebar-service/sidebar.service"; import { ApiKeyInterceptor } from "./shared/interceptors/api-key.interceptor"; import { XSRFInterceptor } from "./shared/interceptors/xsrf.interceptor"; -import { IdentitiesOverviewComponent } from "./components/shared/identities-overview/identities-overview.component"; @NgModule({ declarations: [ @@ -125,7 +126,8 @@ import { IdentitiesOverviewComponent } from "./components/shared/identities-over LayoutModule, MatDialogModule, MatSelectModule, - MatChipsModule + MatChipsModule, + MatProgressBarModule ], providers: [ SidebarService, diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.css b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.css index 1a0517f917..4580a65f9a 100644 --- a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.css +++ b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.css @@ -154,3 +154,12 @@ .auto-height { height: auto; } + +.progressWrapper { + display: flex; + align-items: center; +} + +.progressWrapper > span { + min-width: 2.5rem; +} diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.html b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.html index 32fb78195e..859c1b7b87 100644 --- a/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.html +++ b/AdminUi/src/AdminUi/ClientApp/src/app/components/quotas/identity/identity-edit/identity-edit.component.html @@ -93,9 +93,12 @@

{{ header }}

- Max - - {{ Quota.max }} + Used/Max + +
+ {{ Quota.usage }}/{{ Quota.max }} + +
diff --git a/AdminUi/src/AdminUi/ClientApp/src/app/services/quotas-service/quotas.service.ts b/AdminUi/src/AdminUi/ClientApp/src/app/services/quotas-service/quotas.service.ts index 0a6055ccc0..33861a0064 100644 --- a/AdminUi/src/AdminUi/ClientApp/src/app/services/quotas-service/quotas.service.ts +++ b/AdminUi/src/AdminUi/ClientApp/src/app/services/quotas-service/quotas.service.ts @@ -63,6 +63,7 @@ export interface Quota { source: "Tier" | "Individual"; metric: Metric; max: number; + usage: number; period: string; disabled: boolean; deleteable: boolean; diff --git a/Modules/Quotas/src/Quotas.Application/DTOs/QuotaDTO.cs b/Modules/Quotas/src/Quotas.Application/DTOs/QuotaDTO.cs index f3fd00eb5a..ba6718d9f6 100644 --- a/Modules/Quotas/src/Quotas.Application/DTOs/QuotaDTO.cs +++ b/Modules/Quotas/src/Quotas.Application/DTOs/QuotaDTO.cs @@ -3,18 +3,20 @@ namespace Backbone.Modules.Quotas.Application.DTOs; public class QuotaDTO { - public QuotaDTO(string id, QuotaSource source, MetricDTO metric, int max, string period) + public QuotaDTO(Quota quota, MetricDTO metric, uint usage) { - Id = id; - Source = source; + Id = quota.Id; + Source = quota is TierQuota ? QuotaSource.Tier : QuotaSource.Individual; Metric = metric; - Max = max; - Period = period; + Max = quota.Max; + Period = quota.Period.ToString(); + Usage = usage; } public string Id { get; set; } public QuotaSource Source { get; set; } public MetricDTO Metric { get; set; } public int Max { get; set; } + public uint Usage { get; set; } public string Period { get; set; } } diff --git a/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs b/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs index b069ec738f..b545c932d5 100644 --- a/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs +++ b/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/GetIdentityResponse.cs @@ -1,36 +1,40 @@ using Backbone.Modules.Quotas.Application.DTOs; using Backbone.Modules.Quotas.Domain.Aggregates.Identities; using Backbone.Modules.Quotas.Domain.Aggregates.Metrics; +using Backbone.Modules.Quotas.Domain.Metrics; namespace Backbone.Modules.Quotas.Application.Identities.Queries.GetIdentity; public class GetIdentityResponse { - public GetIdentityResponse(string identityAddress, IEnumerable tierQuotas, IEnumerable individualQuotas, IEnumerable metrics) + public string Address { get; set; } + public IEnumerable Quotas { get; set; } + + public static async Task Create(MetricCalculatorFactory metricCalculatorFactory, string identityAddress, IEnumerable tierQuotas, IEnumerable individualQuotas, IEnumerable metrics, CancellationToken cancellationToken) { var quotasList = new List(); - quotasList.AddRange(individualQuotas.Select(q => - new QuotaDTO( - q.Id, - QuotaSource.Individual, - new MetricDTO(metrics.First(m => m.Key == q.MetricKey)), - q.Max, - q.Period.ToString() - ) - )); - quotasList.AddRange(tierQuotas.Select(q => - new QuotaDTO( - q.Id, - QuotaSource.Tier, - new MetricDTO(metrics.First(m => m.Key == q.MetricKey)), - q.Max, - q.Period.ToString() - ) - )); - Address = identityAddress; - Quotas = quotasList; + var allQuotas = (individualQuotas as IEnumerable).Union(tierQuotas); + + foreach (var quota in allQuotas) + { + var usage = await CalculateUsage(metricCalculatorFactory, quota, identityAddress, cancellationToken); + quotasList.Add(new QuotaDTO( + quota, + new MetricDTO(metrics.First(m => m.Key == quota.MetricKey)), + usage + )); + } + + return new GetIdentityResponse() + { + Address = identityAddress, + Quotas = quotasList + }; } - public string Address { get; set; } - public IEnumerable Quotas { get; set; } + private static async Task CalculateUsage(MetricCalculatorFactory metricCalculatorFactory, Quota quota, string identityAddress, CancellationToken cancellationToken) + { + var calculator = metricCalculatorFactory.CreateFor(quota.MetricKey); + return await calculator.CalculateUsage(quota.Period.CalculateBegin(), quota.Period.CalculateEnd(), identityAddress, cancellationToken); + } } diff --git a/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/Handler.cs b/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/Handler.cs index 62b98dee11..d890777b81 100644 --- a/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/Handler.cs +++ b/Modules/Quotas/src/Quotas.Application/Identities/Queries/GetIdentity/Handler.cs @@ -1,6 +1,7 @@ using Backbone.BuildingBlocks.Application.Abstractions.Exceptions; using Backbone.Modules.Quotas.Application.Infrastructure.Persistence.Repository; using Backbone.Modules.Quotas.Domain.Aggregates.Identities; +using Backbone.Modules.Quotas.Domain.Metrics; using MediatR; namespace Backbone.Modules.Quotas.Application.Identities.Queries.GetIdentity; @@ -8,11 +9,13 @@ public class Handler : IRequestHandler { private readonly IIdentitiesRepository _identitiesRepository; private readonly IMetricsRepository _metricsRepository; + private readonly MetricCalculatorFactory _metricCalculatorFactory; - public Handler(IIdentitiesRepository identitiesRepository, IMetricsRepository metricsRepository) + public Handler(IIdentitiesRepository identitiesRepository, IMetricsRepository metricsRepository, MetricCalculatorFactory metricCalculatorFactory) { _identitiesRepository = identitiesRepository; _metricsRepository = metricsRepository; + _metricCalculatorFactory = metricCalculatorFactory; } public async Task Handle(GetIdentityQuery request, CancellationToken cancellationToken) @@ -21,7 +24,6 @@ public async Task Handle(GetIdentityQuery request, Cancella var metricsKeys = identity.TierQuotas.Select(q => q.MetricKey).Union(identity.IndividualQuotas.Select(q => q.MetricKey)); var metrics = await _metricsRepository.FindAllWithKeys(metricsKeys, cancellationToken); - - return new GetIdentityResponse(identity.Address, identity.TierQuotas, identity.IndividualQuotas, metrics); + return await GetIdentityResponse.Create(_metricCalculatorFactory, identity.Address, identity.TierQuotas, identity.IndividualQuotas, metrics, cancellationToken); } } diff --git a/Modules/Quotas/test/Quotas.Application.Tests/Tests/Identities/GetIdentity/HandlerTests.cs b/Modules/Quotas/test/Quotas.Application.Tests/Tests/Identities/GetIdentity/HandlerTests.cs index f6db89e0cd..9adfab9233 100644 --- a/Modules/Quotas/test/Quotas.Application.Tests/Tests/Identities/GetIdentity/HandlerTests.cs +++ b/Modules/Quotas/test/Quotas.Application.Tests/Tests/Identities/GetIdentity/HandlerTests.cs @@ -5,6 +5,7 @@ using Backbone.Modules.Quotas.Domain.Aggregates.Identities; using Backbone.Modules.Quotas.Domain.Aggregates.Metrics; using Backbone.Modules.Quotas.Domain.Aggregates.Tiers; +using Backbone.Modules.Quotas.Domain.Metrics; using Backbone.UnitTestTools.Extensions; using FakeItEasy; using FluentAssertions; @@ -29,10 +30,10 @@ public async void Gets_identity_quotas_by_address() var stubMetricsRepository = new FindAllWithKeysMetricsStubRepository(new List { metric }); - var identitiesRepository = A.Fake(); - A.CallTo(() => identitiesRepository.Find(A._, A._, A._)).Returns(identity); + var stubIdentitiesRepository = A.Fake(); + A.CallTo(() => stubIdentitiesRepository.Find(A._, A._, A._)).Returns(identity); - var handler = CreateHandler(identitiesRepository, stubMetricsRepository); + var handler = CreateHandler(stubIdentitiesRepository, stubMetricsRepository); // Act var result = await handler.Handle(new GetIdentityQuery(identity.Address), CancellationToken.None); @@ -58,11 +59,10 @@ public async void Gets_identity_quotas_by_address() public void Fails_when_no_identity_found() { // Arrange - var metricsRepository = A.Fake(); - var identitiesRepository = A.Fake(); - A.CallTo(() => identitiesRepository.Find(A._, A._, A._)).Returns((Identity)null); + var stubIdentitiesRepository = A.Fake(); + A.CallTo(() => stubIdentitiesRepository.Find(A._, A._, A._)).Returns((Identity)null); - var handler = CreateHandler(identitiesRepository, metricsRepository); + var handler = CreateHandler(stubIdentitiesRepository); // Act Func acting = async () => await handler.Handle(new GetIdentityQuery("some-inexistent-identity-address"), CancellationToken.None); @@ -73,8 +73,15 @@ public void Fails_when_no_identity_found() exception.Code.Should().Be("error.platform.recordNotFound"); } - private Handler CreateHandler(IIdentitiesRepository identitiesRepository, IMetricsRepository metricsRepository) + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository) { - return new Handler(identitiesRepository, metricsRepository); + var dummyMetricsRepository = A.Dummy(); + return CreateHandler(identitiesRepository, dummyMetricsRepository); + } + + private static Handler CreateHandler(IIdentitiesRepository identitiesRepository, IMetricsRepository metricsRepository) + { + var dummyMetricCalculatorFactory = A.Dummy(); + return new Handler(identitiesRepository, metricsRepository, dummyMetricCalculatorFactory); } }