Skip to content

Commit

Permalink
Merge pull request #675 from neozhu/onlineusertracker
Browse files Browse the repository at this point in the history
✨ Replace SignalR with ActualLab/Fusion for Online User Tracking and State Updates
  • Loading branch information
neozhu committed May 11, 2024
2 parents 83944ca + 5ae9a62 commit 9b06aca
Show file tree
Hide file tree
Showing 32 changed files with 1,189 additions and 913 deletions.
5 changes: 5 additions & 0 deletions src/Infrastructure/Extensions/SerilogExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ public static void RegisterSerilog(this WebApplicationBuilder builder)
.MinimumLevel.Override("Hangfire.Server.ServerHeartbeatProcess", LogEventLevel.Error)
.MinimumLevel.Override("Hangfire.Processing.BackgroundExecution", LogEventLevel.Error)
.MinimumLevel.Override("ZiggyCreatures.Caching.Fusion.FusionCache", LogEventLevel.Error)
.MinimumLevel.Override("ActualLab.Fusion.Interception.ComputeServiceInterceptor", LogEventLevel.Error)
.MinimumLevel.Override("ActualLab.Fusion.Extensions.Services.InMemoryKeyValueStore", LogEventLevel.Error)
.MinimumLevel.Override("ActualLab.Fusion.Operations.Internal.CompletionProducer", LogEventLevel.Error)
.MinimumLevel.Override("CleanArchitecture.Blazor.Server.UI.Services.Fusion.UserSessionTracker", LogEventLevel.Error)
.MinimumLevel.Override("CleanArchitecture.Blazor.Server.UI.Services.Fusion.OnlineUserTracker", LogEventLevel.Error)
.Enrich.FromLogContext()
.Enrich.WithUtcTime()
.WriteTo.Async(wt => wt.File("./log/log-.txt", rollingInterval: RollingInterval.Day))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ private async Task TrySeedAsync()
}

// Default roles
var administratorRole = new ApplicationRole(RoleName.Admin) { Description = "Admin Group" };
var userRole = new ApplicationRole(RoleName.Basic) { Description = "Basic Group" };
var administratorRole = new ApplicationRole(RoleName.Admin) { Description = "Admin Group", TenantId= _context.Tenants.First().Id };
var userRole = new ApplicationRole(RoleName.Basic) { Description = "Basic Group", TenantId = _context.Tenants.First().Id };
var permissions = GetAllPermissions();
if (_roleManager.Roles.All(r => r.Name != administratorRole.Name))
{
Expand Down
6 changes: 3 additions & 3 deletions src/Server.UI/Components/Dialogs/DeleteConfirmation.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
<MudIcon Icon="@Icons.Material.Filled.Delete" Class="mr-3 mb-n1"/>
<MudIcon Icon="@Icons.Material.Filled.Delete" Class="mr-3 mb-n1" />
@ConstantString.DeleteConfirmationTitle
</MudText>
</TitleContent>
Expand All @@ -20,8 +20,8 @@
{
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;

[EditorRequired] [Parameter] public string? ContentText { get; set; }
[EditorRequired] [Parameter] public IRequest<Result<int>> Command { get; set; } = default!;
[EditorRequired][Parameter] public string? ContentText { get; set; }
[EditorRequired][Parameter] public IRequest<Result<int>> Command { get; set; } = default!;

private async Task Submit()
{
Expand Down
5 changes: 2 additions & 3 deletions src/Server.UI/Components/Fusion/ActiveUserSession.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
@using ActualLab.Fusion.Blazor
@using ActualLab.Fusion.UI
@inherits ComputedStateComponent<string[]>
@implements IAsyncDisposable
@inject IUserSessionTracker UserSessionTracker
@inject IStringLocalizer<ActiveUserSession> L

Expand All @@ -17,7 +16,7 @@
[Parameter]
public string PageComponent { get; set; } = nameof(ActiveUserSession);
private string Message => $"{string.Join(", ", State.Value)} {L["has this dialog open."]}";
private string userName;
private string? userName;
[CascadingParameter]
private Task<AuthenticationState> AuthState { get; set; } = default!;
[Inject] private UIActionTracker UIActionTracker { get; init; } = null!;
Expand Down Expand Up @@ -45,7 +44,7 @@
return Array.Empty<string>();
}

public async ValueTask DisposeAsync()
public override async ValueTask DisposeAsync()
{
await UserSessionTracker.RemoveUserSession(PageComponent, userName);
GC.Collect();
Expand Down
78 changes: 78 additions & 0 deletions src/Server.UI/Components/Fusion/OnlineUsersTracker.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@using CleanArchitecture.Blazor.Server.UI.Services.Fusion
@using ActualLab.Fusion
@using ActualLab.Fusion.Blazor
@using ActualLab.Fusion.UI

@inherits ComputedStateComponent<UserInfo[]>
@inject IOnlineUserTracker OnlineUserTracker
@inject IStringLocalizer<ActiveUserSession> L
@implements IAsyncDisposable
@if (State.HasValue && State.LastNonErrorValue.Any())
{
<div class="d-flex flex-row gap-2 my-3 gap-2 my-3">
@foreach (var user in State.LastNonErrorValue.OrderBy(u => u.Id != userId))
{
<MudBadge Color="Color.Success" Overlap="true" Bordered="true">
@if (string.IsNullOrEmpty(user.ProfilePictureDataUrl))
{
<MudAvatar title="@user.Name">
@user.Name.First()
</MudAvatar>
}
else
{
<MudAvatar title="@user.Name">
<MudImage Src="@user.ProfilePictureDataUrl"></MudImage>
</MudAvatar>
}
</MudBadge>
}
</div>
}
@if(State.Error is not null)
{
<MudAlert Severity="Severity.Error">@State.Error.Message</MudAlert>
}


@code {
private Dictionary<string, UserProfile>? onlineUsers;
private string sessionId = Guid.NewGuid().ToString();
private string? userId;
[CascadingParameter]
private Task<AuthenticationState> AuthState { get; set; } = default!;
[Inject] private UIActionTracker UIActionTracker { get; init; } = null!;
private TimeSpan UpdateDelay { get; set; } = TimeSpan.FromSeconds(1);
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
var user = authState.User.GetUserProfileFromClaim();
userId = user.UserId;
await OnlineUserTracker.AddUser(sessionId, new UserInfo(user.UserId,
user.UserName,
user.Email,
user.DisplayName,
user.ProfilePictureDataUrl,
user.SuperiorName,
user.SuperiorId,
user.TenantId,
user.TenantName,
user.PhoneNumber,
user.AssignedRoles ?? Array.Empty<string>(),
UserPresence.Available));
}

protected override ComputedState<UserInfo[]>.Options GetStateOptions()
=> new() { UpdateDelayer = new UpdateDelayer(UIActionTracker, UpdateDelay) };

protected override Task<UserInfo[]> ComputeState(CancellationToken cancellationToken)
{
return OnlineUserTracker.GetOnlineUsers(cancellationToken);
}

public async ValueTask DisposeAsync()
{
await OnlineUserTracker.RemoveUser(sessionId);
GC.Collect();
}
}
6 changes: 5 additions & 1 deletion src/Server.UI/Components/Identity/UserLoginState.razor
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
@using CleanArchitecture.Blazor.Server.UI.Hubs
@using CleanArchitecture.Blazor.Server.UI.Services.Fusion
@using CleanArchitecture.Blazor.Server.UI.Hubs
@using CleanArchitecture.Blazor.Application.Common.Interfaces.Identity
@inherits FluxorComponent

@inject IState<UserProfileState> UserProfileState
@inject IStringLocalizer<SharedResource> L
@inject IDispatcher Dispatcher

@code {

[CascadingParameter] protected Task<AuthenticationState> AuthState { get; set; } = default!;
Expand All @@ -25,6 +28,7 @@

protected override async Task OnInitializedAsync()
{

Client.LoginEvent += _client_Login;
Client.LogoutEvent += _client_Logout;
UserProfileState.StateChanged += OnStateChanged;
Expand Down
1 change: 1 addition & 0 deletions src/Server.UI/Components/Shared/Layout/AppLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
</ErrorBoundary>
</MudContainer>
</MudMainContent>

<UserLoginState UserProfileStateChanged="OnUserProfileStateChanged" />
</Authorized>
</AuthorizeView>
Expand Down
7 changes: 0 additions & 7 deletions src/Server.UI/Components/Shared/Layout/UserMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
@inject IState<UserProfileState> UserProfileState
@inject IStringLocalizer<HeaderMenu> L


<MudTooltip Arrow="true" Text="@L["User Profile"]">
<MudMenu AnchorOrigin="Origin.BottomRight"
Icon="@Icons.Material.Filled.PermIdentity"
Expand Down Expand Up @@ -80,12 +79,6 @@
@code
{
[Parameter] public EventCallback<EventArgs> OnSettingClick { get; set; }
[Inject] public AuthenticationStateProvider AuthenticationStateProvider { get; set; } = null!;
private bool IsLoading => UserProfileState.Value.IsLoading;
private UserProfile? UserProfile => UserProfileState.Value.UserProfile;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
GC.SuppressFinalize(this);
}
}
7 changes: 6 additions & 1 deletion src/Server.UI/Components/Shared/Themes/ThemesMenu.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@using CleanArchitecture.Blazor.Server.UI.Services.UserPreferences
@using CleanArchitecture.Blazor.Server.UI.Components.Fusion
@using CleanArchitecture.Blazor.Server.UI.Services.UserPreferences
@using System.Globalization
@inject LayoutService LayoutService
@inject IStringLocalizer<ThemesMenu> L
Expand Down Expand Up @@ -134,6 +135,10 @@
</MudItem>

</MudGrid>
<MudText Typo="Typo.body2">
<b>@L["Online Users"]</b>
</MudText>
<OnlineUsersTracker></OnlineUsersTracker>
</div>
</MudDrawer>

Expand Down
3 changes: 3 additions & 0 deletions src/Server.UI/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
using QuestPDF.Infrastructure;
using ActualLab.Fusion;
using Toolbelt.Blazor.Extensions.DependencyInjection;
using ActualLab.Fusion.Extensions;

namespace CleanArchitecture.Blazor.Server.UI;

Expand Down Expand Up @@ -58,7 +59,9 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I

// Fusion services
services.AddFusion(fusion => {
fusion.AddInMemoryKeyValueStore();
fusion.AddService<IUserSessionTracker,UserSessionTracker>();
fusion.AddService<IOnlineUserTracker, OnlineUserTracker>();
});


Expand Down
1 change: 0 additions & 1 deletion src/Server.UI/Pages/Dashboard/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

<MudContainer MaxWidth="MaxWidth.Large">
<div class="@ContentClassNames">

<div class="lp-app-grid">
<MudPaper Class="py-4 px-6 rounded-lg d-flex align-center">
<div class="flex-grow-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@
</DialogActions>
</MudDialog>

@code{
@code {
[CascadingParameter] private MudDialogInstance MudDialog { get; set; } = default!;

[EditorRequired] [Parameter] public AddEditDocumentCommand Model { get; set; } = default!;
[EditorRequired][Parameter] public AddEditDocumentCommand Model { get; set; } = default!;

private MudForm? _form;

Expand Down
19 changes: 14 additions & 5 deletions src/Server.UI/Pages/Identity/Users/Components/UserForm.razor
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,22 @@
<MudItem xs="12" sm="12">
<MudText Typo="Typo.caption">@L["Assign Roles"]</MudText>
<MudGrid Class="mt-1">
@for (var i = 0; i < Roles.Count; i++)
@if (Roles.Any())
{
var x = i;
<MudItem xs="6" sm="6" Class="py-0 my=0">
<MudCheckBox T="bool" Value="@Roles[x].Value" Label="@Roles[x].Key" ValueChanged="@(s => Roles[x].Value = s)"></MudCheckBox>
</MudItem>
@for (var i = 0; i < Roles.Count; i++)
{
var x = i;
<MudItem xs="6" sm="6" Class="py-0 my=0">
<MudCheckBox T="bool" Value="@Roles[x].Value" Label="@Roles[x].Key" ValueChanged="@(s => Roles[x].Value = s)"></MudCheckBox>
</MudItem>
}

}
else
{
<MudAlert Class="flex-grow-1" Severity="Severity.Warning">@L["No roles available for this tenant."]</MudAlert>
}

</MudGrid>
</MudItem>
</MudGrid>
Expand Down
Loading

0 comments on commit 9b06aca

Please sign in to comment.