Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add user summary metrics to Admin Dashboard #669

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions application/account-management/Api/Endpoints/UserEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,9 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
group.MapPut("/change-locale", async Task<ApiResult> (ChangeLocaleCommand command, IMediator mediator)
=> await mediator.Send(command)
);

group.MapGet("/summary", async Task<ApiResult<GetUserSummaryResponse>> (IMediator mediator)
=> await mediator.Send(new GetUserSummaryQuery())
).Produces<GetUserSummaryResponse>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public sealed record IsSubdomainFreeQuery(string Subdomain) : IRequest<Result<bo
public sealed class IsSubdomainFreeHandler(ITenantRepository tenantRepository)
: IRequestHandler<IsSubdomainFreeQuery, Result<bool>>
{
public async Task<Result<bool>> Handle(IsSubdomainFreeQuery request, CancellationToken cancellationToken)
public async Task<Result<bool>> Handle(IsSubdomainFreeQuery query, CancellationToken cancellationToken)
{
return await tenantRepository.IsSubdomainFreeAsync(request.Subdomain, cancellationToken);
return await tenantRepository.IsSubdomainFreeAsync(query.Subdomain, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public sealed record TenantResponse(TenantId Id, DateTimeOffset CreatedAt, DateT
public sealed class GetTenantHandler(ITenantRepository tenantRepository)
: IRequestHandler<GetTenantQuery, Result<TenantResponse>>
{
public async Task<Result<TenantResponse>> Handle(GetTenantQuery request, CancellationToken cancellationToken)
public async Task<Result<TenantResponse>> Handle(GetTenantQuery query, CancellationToken cancellationToken)
{
var tenant = await tenantRepository.GetByIdAsync(request.Id, cancellationToken);
return tenant?.Adapt<TenantResponse>() ?? Result<TenantResponse>.NotFound($"Tenant with id '{request.Id}' not found.");
var tenant = await tenantRepository.GetByIdAsync(query.Id, cancellationToken);
return tenant?.Adapt<TenantResponse>() ?? Result<TenantResponse>.NotFound($"Tenant with id '{query.Id}' not found.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@

Task<int> CountTenantUsersAsync(TenantId tenantId, CancellationToken cancellationToken);

Task<(int TotalUsers, int ActiveUsers, int PendingUsers)> GetUserSummaryAsync(CancellationToken cancellationToken);

Task<(User[] Users, int TotalItems, int TotalPages)> Search(

Check warning on line 23 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Method has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 23 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Method has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 23 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Method has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)

Check warning on line 23 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Method has 10 parameters, which is greater than the 7 authorized. (https://rules.sonarsource.com/csharp/RSPEC-107)
string? search,
UserRole? userRole,
UserStatus? userStatus,
Expand Down Expand Up @@ -73,7 +75,25 @@
return DbSet.CountAsync(u => u.TenantId == tenantId, cancellationToken);
}

public async Task<(int TotalUsers, int ActiveUsers, int PendingUsers)> GetUserSummaryAsync(CancellationToken cancellationToken)
{
var thirtyDaysAgo = TimeProvider.System.GetUtcNow().AddDays(-30);

var summary = await DbSet
.GroupBy(_ => 1) // Group all records into a single group to calculate multiple COUNT aggregates in one query
.Select(g => new
{
TotalUsers = g.Count(),
ActiveUsers = g.Count(u => u.EmailConfirmed && u.ModifiedAt >= thirtyDaysAgo),
PendingUsers = g.Count(u => !u.EmailConfirmed)
}
)
.SingleAsync(cancellationToken);

return (summary.TotalUsers, summary.ActiveUsers, summary.PendingUsers);
}

public async Task<(User[] Users, int TotalItems, int TotalPages)> Search(

Check warning on line 96 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 96 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 96 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)

Check warning on line 96 in application/account-management/Core/Features/Users/Domain/UserRepository.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Refactor this method to reduce its Cognitive Complexity from 19 to the 15 allowed. (https://rules.sonarsource.com/csharp/RSPEC-3776)
string? search,
UserRole? userRole,
UserStatus? userStatus,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ public sealed record UserResponse(
public sealed class GetUserHandler(IUserRepository userRepository)
: IRequestHandler<GetUserQuery, Result<UserResponse>>
{
public async Task<Result<UserResponse>> Handle(GetUserQuery request, CancellationToken cancellationToken)
public async Task<Result<UserResponse>> Handle(GetUserQuery query, CancellationToken cancellationToken)
{
var user = await userRepository.GetByIdAsync(request.Id, cancellationToken);
return user?.Adapt<UserResponse>() ?? Result<UserResponse>.NotFound($"User with id '{request.Id}' not found.");
var user = await userRepository.GetByIdAsync(query.Id, cancellationToken);
return user?.Adapt<UserResponse>() ?? Result<UserResponse>.NotFound($"User with id '{query.Id}' not found.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using JetBrains.Annotations;
using PlatformPlatform.AccountManagement.Features.Users.Domain;
using PlatformPlatform.SharedKernel.Cqrs;

namespace PlatformPlatform.AccountManagement.Features.Users.Queries;

[PublicAPI]
public sealed record GetUserSummaryQuery : IRequest<Result<GetUserSummaryResponse>>;

[PublicAPI]
public sealed record GetUserSummaryResponse(int TotalUsers, int ActiveUsers, int PendingUsers);

public sealed class GetUserSummaryHandler(IUserRepository userRepository)
: IRequestHandler<GetUserSummaryQuery, Result<GetUserSummaryResponse>>
{
public async Task<Result<GetUserSummaryResponse>> Handle(GetUserSummaryQuery query, CancellationToken cancellationToken)
{
var (totalUsers, activeUsers, pendingUsers) = await userRepository.GetUserSummaryAsync(cancellationToken);
return new GetUserSummaryResponse(totalUsers, activeUsers, pendingUsers);
}
}
17 changes: 14 additions & 3 deletions application/account-management/WebApp/routes/admin/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const Route = createFileRoute("/admin/")({
});

export default function Home() {
const { data } = useApi("/api/account-management/users", { params: { query: { PageSize: 1 } } });
const { data } = useApi("/api/account-management/users/summary", {});

return (
<div className="flex gap-4 w-full h-full">
Expand All @@ -35,7 +35,7 @@ export default function Home() {
<Trans>Add more in the Users menu</Trans>
</div>
<div className="py-2 text-black text-2xl font-semibold">
{data?.totalCount ? <p>{data?.totalCount}</p> : <p>-</p>}
{data?.totalUsers ? <p>{data.totalUsers}</p> : <p>-</p>}
</div>
</div>
<div className="p-6 bg-white rounded-xl shadow-md w-1/3 mx-6">
Expand All @@ -46,7 +46,18 @@ export default function Home() {
<Trans>Active users in the past 30 days</Trans>
</div>
<div className="py-2 text-black text-2xl font-semibold">
{data?.totalCount ? <p>{data?.totalCount}</p> : <p>-</p>}
{data?.activeUsers ? <p>{data.activeUsers}</p> : <p>-</p>}
</div>
</div>
<div className="p-6 bg-white rounded-xl shadow-md w-1/3">
<div className="text-sm text-gray-800">
<Trans>Invited Users</Trans>
</div>
<div className="text-sm text-gray-500">
<Trans>Users who haven't confirmed their email</Trans>
</div>
<div className="py-2 text-black text-2xl font-semibold">
{data?.pendingUsers ? <p>{data.pendingUsers}</p> : <p>-</p>}
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,26 @@
}
}
}
},
"/api/account-management/users/summary": {
"get": {
"tags": [
"Users"
],
"operationId": "GetApiAccountManagementUsersSummary",
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/GetUserSummaryResponse"
}
}
}
}
}
}
}
},
"components": {
Expand Down Expand Up @@ -1152,6 +1172,24 @@
"type": "string"
}
}
},
"GetUserSummaryResponse": {
"type": "object",
"additionalProperties": false,
"properties": {
"totalUsers": {
"type": "integer",
"format": "int32"
},
"activeUsers": {
"type": "integer",
"format": "int32"
},
"pendingUsers": {
"type": "integer",
"format": "int32"
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ msgstr "Inviter Brugere"
msgid "Invite users and assign them roles. They will appear once they log in."
msgstr "Inviter brugere og tildel dem roller. De vil blive vist, når de logger ind."

msgid "Invited Users"
msgstr "Inviterede brugere"

msgid "Last name"
msgstr "Efternavn"

Expand Down Expand Up @@ -334,6 +337,9 @@ msgstr "[email protected]"
msgid "Users"
msgstr "Brugere"

msgid "Users who haven't confirmed their email"
msgstr "Brugere, der ikke har bekræftet deres e-mail"

msgid "Verify"
msgstr "Bekræft"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ msgstr "Invite Users"
msgid "Invite users and assign them roles. They will appear once they log in."
msgstr "Invite users and assign them roles. They will appear once they log in."

msgid "Invited Users"
msgstr "Invited Users"

msgid "Last name"
msgstr "Last name"

Expand Down Expand Up @@ -334,6 +337,9 @@ msgstr "[email protected]"
msgid "Users"
msgstr "Users"

msgid "Users who haven't confirmed their email"
msgstr "Users who haven't confirmed their email"

msgid "Verify"
msgstr "Verify"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,9 @@ msgstr "Gebruikers uitnodigen"
msgid "Invite users and assign them roles. They will appear once they log in."
msgstr "Nodig gebruikers uit en wijs hen rollen toe. Ze verschijnen zodra ze inloggen."

msgid "Invited Users"
msgstr "Uitgenodigde gebruikers"

msgid "Last name"
msgstr "Achternaam"

Expand Down Expand Up @@ -334,6 +337,9 @@ msgstr "[email protected]"
msgid "Users"
msgstr "Gebruikers"

msgid "Users who haven't confirmed their email"
msgstr "Gebruikers die hun e-mail niet hebben bevestigd"

msgid "Verify"
msgstr "Verifiëren"

Expand Down
2 changes: 1 addition & 1 deletion application/shared-webapp/infrastructure/auth/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ export const loginPath = "/login";
/**
* Path to the default page shown after successful login
*/
export const loggedInPath = "/admin/users";
export const loggedInPath = "/admin";
Loading