From fceb1efcf4ab859e98352c3244ffd7842a6ea250 Mon Sep 17 00:00:00 2001 From: "hualin.zhu" Date: Thu, 31 Oct 2024 21:44:34 +0800 Subject: [PATCH 1/6] remove AddInMemoryKeyValueStore --- src/Server.UI/DependencyInjection.cs | 11 +++-------- src/Server.UI/Hubs/HubClient.cs | 10 +++++++--- src/Server.UI/Server.UI.csproj | 18 +++++------------- 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index 4accf3b6d..ef0fbe180 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -17,7 +17,6 @@ using QuestPDF.Infrastructure; using ActualLab.Fusion; using Toolbelt.Blazor.Extensions.DependencyInjection; -using ActualLab.Fusion.Extensions; using CleanArchitecture.Blazor.Server.UI.Middlewares; using Polly; @@ -66,12 +65,9 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I services.AddHotKeys2(); // Fusion services - services.AddFusion(fusion => - { - fusion.AddInMemoryKeyValueStore(); - fusion.AddService(); - fusion.AddService(); - }); + var fusion = services.AddFusion(); + fusion.AddService(); + fusion.AddService(); services.AddScoped() @@ -150,7 +146,6 @@ public static WebApplication ConfigureServer(this WebApplication app, IConfigura app.UseStatusCodePagesWithRedirects("/404"); app.MapHealthChecks("/health"); - app.UseAuthentication(); app.UseAuthorization(); app.UseAntiforgery(); diff --git a/src/Server.UI/Hubs/HubClient.cs b/src/Server.UI/Hubs/HubClient.cs index 51fedfb97..f27acc367 100644 --- a/src/Server.UI/Hubs/HubClient.cs +++ b/src/Server.UI/Hubs/HubClient.cs @@ -1,4 +1,5 @@ using System.Net; +using CleanArchitecture.Blazor.Infrastructure.Constants.User; using Microsoft.AspNetCore.Http.Connections; using Microsoft.AspNetCore.SignalR.Client; @@ -35,7 +36,8 @@ public HubClient(NavigationManager navigationManager, IHttpContextAccessor httpC options.Cookies = container; }).WithAutomaticReconnect().Build(); - _hubConnection.ServerTimeout = TimeSpan.FromSeconds(30); + _hubConnection.ServerTimeout = TimeSpan.FromSeconds(20); + _hubConnection.KeepAliveInterval = TimeSpan.FromSeconds(10); // Observe and await the async result of the event invocation _hubConnection.On(nameof(ISignalRHub.Connect), OnLoginEventAsync); @@ -52,6 +54,8 @@ public HubClient(NavigationManager navigationManager, IHttpContextAccessor httpC _hubConnection.On(nameof(ISignalRHub.SendPrivateMessage), async (from, to, message) => await OnMessageReceivedEventAsync(from, message).ConfigureAwait(false)); + + } // Handle the result of async event invocations @@ -102,7 +106,7 @@ private async Task OnMessageReceivedEventAsync( string from,string message) await Task.Run(() => MessageReceivedEvent?.Invoke(this, new MessageReceivedEventArgs(from, message))).ConfigureAwait(false); } } - + public async ValueTask DisposeAsync() { try @@ -114,7 +118,7 @@ public async ValueTask DisposeAsync() await _hubConnection.DisposeAsync().ConfigureAwait(false); } } - + // Event handlers public event EventHandler? LoginEvent; public event EventHandler? LogoutEvent; diff --git a/src/Server.UI/Server.UI.csproj b/src/Server.UI/Server.UI.csproj index 2c044fbfc..635943d1b 100644 --- a/src/Server.UI/Server.UI.csproj +++ b/src/Server.UI/Server.UI.csproj @@ -15,28 +15,20 @@ default - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + From 5a6453b92aab274f8cfa8a30330bc7b4d940d2ef Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 1 Nov 2024 11:22:25 +0800 Subject: [PATCH 2/6] fixed --- .../Components/Fusion/ActiveUserSession.razor | 18 +- .../Fusion/OnlineUsersTracker.razor | 31 ++-- src/Server.UI/DependencyInjection.cs | 3 +- .../Middlewares/UserSessionCircuitHandler.cs | 38 +++++ .../Services/Fusion/IOnlineUserTracker.cs | 38 +---- .../Services/Fusion/IUserSessionTracker.cs | 36 +++- .../Services/Fusion/OnlineUserTracker.cs | 160 ++++++------------ .../Services/Fusion/UserSessionTracker.cs | 107 +++++++++--- 8 files changed, 237 insertions(+), 194 deletions(-) create mode 100644 src/Server.UI/Middlewares/UserSessionCircuitHandler.cs diff --git a/src/Server.UI/Components/Fusion/ActiveUserSession.razor b/src/Server.UI/Components/Fusion/ActiveUserSession.razor index e3e98452f..37dc8773f 100644 --- a/src/Server.UI/Components/Fusion/ActiveUserSession.razor +++ b/src/Server.UI/Components/Fusion/ActiveUserSession.razor @@ -6,16 +6,16 @@ @inject IUserSessionTracker UserSessionTracker @inject IStringLocalizer L -@if (!string.IsNullOrEmpty(userName) && State.Value.Any()) +@if (!string.IsNullOrEmpty(currentUserId) && State.Value.Any()) { - @Message + @Message } @code { [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? currentUserId; [CascadingParameter] private Task AuthState { get; set; } = default!; [Inject] private UIActionTracker UIActionTracker { get; init; } = null!; @@ -23,8 +23,10 @@ protected override async Task OnInitializedAsync() { var authState = await AuthState; - userName = authState.User.GetDisplayName() ?? (authState.User.GetUserName()??string.Empty); - await UserSessionTracker.AddUserSession(PageComponent ?? nameof(ActiveUserSession), userName); + currentUserId = authState.User.GetUserId() ?? string.Empty; + await UserSessionTracker.AddUserSession(PageComponent ?? nameof(ActiveUserSession)); + + } protected override ComputedState.Options GetStateOptions() @@ -32,11 +34,11 @@ protected override async Task ComputeState(CancellationToken cancellationToken) { - var result = await UserSessionTracker.GetUserSessions(cancellationToken); + var result = await UserSessionTracker.GetUserSessions(PageComponent,cancellationToken); if (result.Any()) { - return result.Where(x => x.PageComponent == PageComponent).Where(x => x.UserSessions.Any(y => y != userName)).SelectMany(x => x.UserSessions.Where(y => y != userName)).ToArray(); + return result.Where(x => x.UserId!=currentUserId).Select(x=>x.DisplayName??x.UserName).ToArray(); } return Array.Empty(); @@ -44,7 +46,7 @@ public override async ValueTask DisposeAsync() { - await UserSessionTracker.RemoveUserSession(PageComponent, userName??string.Empty); + await UserSessionTracker.RemoveUserSession(PageComponent); GC.Collect(); } } diff --git a/src/Server.UI/Components/Fusion/OnlineUsersTracker.razor b/src/Server.UI/Components/Fusion/OnlineUsersTracker.razor index 2c5978188..fe79e4f2f 100644 --- a/src/Server.UI/Components/Fusion/OnlineUsersTracker.razor +++ b/src/Server.UI/Components/Fusion/OnlineUsersTracker.razor @@ -3,25 +3,25 @@ @using ActualLab.Fusion.Blazor @using ActualLab.Fusion.UI -@inherits ComputedStateComponent +@inherits ComputedStateComponent> @inject IOnlineUserTracker OnlineUserTracker @inject IStringLocalizer L @inject UserProfileStateService UserProfileStateService @if (State.HasValue && State.LastNonErrorValue.Any()) {
- @foreach (var user in State.LastNonErrorValue.OrderBy(u => u.Name != currentUserName)) + @foreach (var user in State.LastNonErrorValue.OrderBy(u => u.UserId != currentUserId)) { @if (string.IsNullOrEmpty(user.ProfilePictureDataUrl)) { - - @user.Name.First() + + @user.UserName.First() } else { - + } @@ -38,7 +38,7 @@ @code { private string sessionId = Guid.NewGuid().ToString(); - private string? currentUserName; + private string? currentUserId; [CascadingParameter] private Task AuthState { get; set; } = default!; [Inject] private UIActionTracker UIActionTracker { get; init; } = null!; private TimeSpan UpdateDelay { get; set; } = TimeSpan.FromSeconds(5); @@ -49,33 +49,28 @@ { await UserProfileStateService.InitializeAsync(state.User.Identity.Name); var userProfile = UserProfileStateService.UserProfile; - currentUserName = userProfile.UserName; - await OnlineUserTracker.Add(sessionId, new UserInfo(userProfile.UserId, + currentUserId = userProfile.UserId; + await OnlineUserTracker.Initial(new SessionInfo(userProfile.UserId, userProfile.UserName, - userProfile.Email, userProfile.DisplayName ?? string.Empty, + "", + userProfile.TenantId, userProfile.ProfilePictureDataUrl ?? string.Empty, - userProfile.SuperiorName ?? string.Empty, - userProfile.SuperiorId ?? string.Empty, - userProfile.TenantId ?? string.Empty, - userProfile.TenantName ?? string.Empty, - userProfile.PhoneNumber, - userProfile.AssignedRoles ?? Array.Empty(), UserPresence.Available)); } } - protected override ComputedState.Options GetStateOptions() + protected override ComputedState>.Options GetStateOptions() => new() { UpdateDelayer = new UpdateDelayer(UIActionTracker, UpdateDelay) }; - protected override Task ComputeState(CancellationToken cancellationToken) + protected override Task> ComputeState(CancellationToken cancellationToken) { return OnlineUserTracker.GetOnlineUsers(cancellationToken); } public override async ValueTask DisposeAsync() { - await OnlineUserTracker.Remove(sessionId); + await OnlineUserTracker.Logout(); GC.Collect(); } } diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index ef0fbe180..0557d8a22 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -19,6 +19,7 @@ using Toolbelt.Blazor.Extensions.DependencyInjection; using CleanArchitecture.Blazor.Server.UI.Middlewares; using Polly; +using Microsoft.AspNetCore.Components.Server.Circuits; namespace CleanArchitecture.Blazor.Server.UI; @@ -68,7 +69,7 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I var fusion = services.AddFusion(); fusion.AddService(); fusion.AddService(); - + services.AddScoped(); services.AddScoped() .Configure(options => diff --git a/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs b/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs new file mode 100644 index 000000000..c00b583a8 --- /dev/null +++ b/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs @@ -0,0 +1,38 @@ +using CleanArchitecture.Blazor.Server.UI.Services.Fusion; +using Microsoft.AspNetCore.Components.Server.Circuits; + +namespace CleanArchitecture.Blazor.Server.UI.Middlewares; + +public class UserSessionCircuitHandler : CircuitHandler +{ + private readonly IUserSessionTracker _userSessionTracker; + private readonly IOnlineUserTracker _onlineUserTracker; + private readonly IHttpContextAccessor _httpContextAccessor; + + + public UserSessionCircuitHandler(IUserSessionTracker userSessionTracker, IOnlineUserTracker OnlineUserTracker, IHttpContextAccessor httpContextAccessor) + { + _userSessionTracker = userSessionTracker; + _onlineUserTracker = OnlineUserTracker; + _httpContextAccessor = httpContextAccessor; + + } + + public override async Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) + { + await base.OnConnectionUpAsync(circuit, cancellationToken); + } + + public override async Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken) + { + var userId = _httpContextAccessor.HttpContext?.User.GetUserId(); + + if (!string.IsNullOrEmpty(userId)) + { + await _userSessionTracker.RemoveAllSessions(userId, cancellationToken); + await _onlineUserTracker.Clear(userId, cancellationToken); + } + + await base.OnConnectionDownAsync(circuit, cancellationToken); + } +} diff --git a/src/Server.UI/Services/Fusion/IOnlineUserTracker.cs b/src/Server.UI/Services/Fusion/IOnlineUserTracker.cs index 082d4db37..5b7800b3f 100644 --- a/src/Server.UI/Services/Fusion/IOnlineUserTracker.cs +++ b/src/Server.UI/Services/Fusion/IOnlineUserTracker.cs @@ -1,42 +1,16 @@ -using System.Runtime.Serialization; -using ActualLab.Fusion; -using ActualLab.Fusion.Blazor; -using MemoryPack; +using ActualLab.Fusion; namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; public interface IOnlineUserTracker : IComputeService { - Task Add(string sessionId, UserInfo userInfo, CancellationToken cancellationToken = default); - Task Remove(string sessionId, CancellationToken cancellationToken = default); + Task Initial(SessionInfo sessionInfo,CancellationToken cancellationToken = default); + Task Logout(CancellationToken cancellationToken = default); + Task Clear(string userId,CancellationToken cancellationToken = default); Task Update(string userId,string userName,string displayName,string profilePictureDataUrl, CancellationToken cancellationToken = default); [ComputeMethod] - Task GetOnlineUsers(CancellationToken cancellationToken = default); + Task> GetOnlineUsers(CancellationToken cancellationToken = default); } -[DataContract, MemoryPackable] -[ParameterComparer(typeof(ByValueParameterComparer))] -public sealed partial record UserInfo( - [property: DataMember] string Id, - [property: DataMember] string Name, - [property: DataMember] string Email, - [property: DataMember] string DisplayName, - [property: DataMember] string ProfilePictureDataUrl, - [property: DataMember] string SuperiorName, - [property: DataMember] string SuperiorId, - [property: DataMember] string TenantId, - [property: DataMember] string TenantName, - [property: DataMember] string? PhoneNumber, - [property: DataMember] string[] AssignedRoles, - [property: DataMember] UserPresence Status -); -public enum UserPresence -{ - Available, - Busy, - Donotdisturb, - Away, - Offline, - Statusunknown -} \ No newline at end of file + \ No newline at end of file diff --git a/src/Server.UI/Services/Fusion/IUserSessionTracker.cs b/src/Server.UI/Services/Fusion/IUserSessionTracker.cs index 7755adbdd..284f43995 100644 --- a/src/Server.UI/Services/Fusion/IUserSessionTracker.cs +++ b/src/Server.UI/Services/Fusion/IUserSessionTracker.cs @@ -1,11 +1,39 @@ -using ActualLab.Fusion; +using System.Runtime.Serialization; +using ActualLab.Fusion; +using ActualLab.Fusion.Blazor; +using MemoryPack; +using MessagePack; namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; public interface IUserSessionTracker: IComputeService { - Task AddUserSession(string pageComponent, string userName, CancellationToken cancellationToken = default); - Task RemoveUserSession(string pageComponent, string userName, CancellationToken cancellationToken = default); + Task AddUserSession(string pageComponent, CancellationToken cancellationToken = default); + Task RemoveUserSession(string pageComponent, CancellationToken cancellationToken = default); + Task RemoveAllSessions(string userId, CancellationToken cancellationToken = default); + [ComputeMethod] - Task<(string PageComponent, string[] UserSessions)[]> GetUserSessions( CancellationToken cancellationToken = default); + Task> GetUserSessions(string pageComponent,CancellationToken cancellationToken = default); } +[DataContract, MemoryPackable, MessagePackObject] +[ParameterComparer(typeof(ByValueParameterComparer))] +public sealed partial record SessionInfo( + [property: DataMember, Key(0)] string UserId, + [property: DataMember, Key(1)] string UserName, + [property: DataMember, Key(2)] string DisplayName, + [property: DataMember, Key(3)] string IPAddress, + [property: DataMember, Key(4)] string TenantId, + [property: DataMember, Key(5)] string ProfilePictureDataUrl, + [property: DataMember, Key(6)] UserPresence Status + + ) +{ } +public enum UserPresence +{ + Available, + Busy, + Donotdisturb, + Away, + Offline, + Statusunknown +} \ No newline at end of file diff --git a/src/Server.UI/Services/Fusion/OnlineUserTracker.cs b/src/Server.UI/Services/Fusion/OnlineUserTracker.cs index ad83fb456..e57f3671f 100644 --- a/src/Server.UI/Services/Fusion/OnlineUserTracker.cs +++ b/src/Server.UI/Services/Fusion/OnlineUserTracker.cs @@ -1,152 +1,98 @@ -using System.Collections.Concurrent; +using System.Collections.Immutable; using System.Linq.Dynamic.Core; using ActualLab.Fusion; + namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; public class OnlineUserTracker : IOnlineUserTracker { - // A concurrent dictionary to store user information by session ID. - private readonly ConcurrentDictionary _store = new(); + public OnlineUserTracker(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + private volatile ImmutableHashSet _activeUserSessions = ImmutableHashSet.Empty; + private readonly IHttpContextAccessor _httpContextAccessor; - /// - /// Adds a user session to the tracker. - /// - /// The session ID. - /// The user information to be added. - /// Optional cancellation token. - public async Task Add(string sessionId, UserInfo userInfo, CancellationToken cancellationToken = default) + public virtual async Task Initial( SessionInfo sessionInfo, CancellationToken cancellationToken = default) { - // If the invalidation is active, skip adding. if (Invalidation.IsActive) return; - - // Try to add the user information into the store. - if (_store.TryAdd(sessionId, userInfo)) + if (!_activeUserSessions.Any(x=>x.UserId==sessionInfo.UserId)) { + _activeUserSessions = _activeUserSessions.Add(sessionInfo); using var invalidating = Invalidation.Begin(); - // Get the updated online users list asynchronously. _ = await GetOnlineUsers(cancellationToken).ConfigureAwait(false); } } - /// - /// Updates a user's information in the tracker. - /// - /// The user's ID. - /// The user's name. - /// The display name. - /// The profile picture URL. - /// Optional cancellation token. - public async Task Update(string userId, string userName, string displayName, string profilePictureDataUrl, CancellationToken cancellationToken = default) + public virtual async Task Logout(CancellationToken cancellationToken = default) { - // If invalidation is active, skip updating. if (Invalidation.IsActive) return; - - var invalidate = false; - - // Loop through the store's keys and update user information based on user name. - foreach (var key in _store.Keys) - { - // Use string.Equals with explicit comparison to avoid using '==' - if (string.Equals(_store[key].Name, userName, StringComparison.Ordinal)) - { - // Update the user information. - var userInfo = _store[key] with - { - Id = userId, - DisplayName = displayName, - ProfilePictureDataUrl = profilePictureDataUrl - }; - - // Try to update the entry in the dictionary. - var updated = _store.TryUpdate(key, userInfo, _store[key]); - if (!invalidate) - { - invalidate = updated; - } - } - } - - // If any user information was updated, invalidate and get the updated list. - if (invalidate) + var sessionInfo = await GetSessionInfo().ConfigureAwait(false); + var userSessions = _activeUserSessions.Where(s => s.UserId == sessionInfo.UserId).ToList(); + foreach (var session in userSessions) { + _activeUserSessions = _activeUserSessions.Remove(session); using var invalidating = Invalidation.Begin(); _ = await GetOnlineUsers(cancellationToken).ConfigureAwait(false); } } - /// - /// Retrieves the list of online users. - /// - /// Optional cancellation token. - /// A task containing the array of online users. - public virtual Task GetOnlineUsers(CancellationToken cancellationToken = default) + public virtual async Task Clear(string userId, CancellationToken cancellationToken = default) { - // Return an empty array if invalidation is active to avoid null return values. if (Invalidation.IsActive) - return Task.FromResult(Array.Empty()); // Avoid returning null - - // Return the distinct list of online users. - return Task.FromResult(_store.Select(x => x.Value).Distinct(new UserInfoEqualityComparer()).ToArray()); + return; + var userSessions = _activeUserSessions.Where(s => s.UserId == userId).ToList(); + foreach (var session in userSessions) + { + _activeUserSessions = _activeUserSessions.Remove(session); + using var invalidating = Invalidation.Begin(); + _ = await GetOnlineUsers(cancellationToken).ConfigureAwait(false); + } } - /// - /// Removes a user session from the tracker. - /// - /// The session ID to remove. - /// Optional cancellation token. - public async Task Remove(string sessionId, CancellationToken cancellationToken = default) + public virtual async Task Update(string userId, string userName, string displayName, string profilePictureDataUrl, CancellationToken cancellationToken = default) { - // If invalidation is active, skip removal. if (Invalidation.IsActive) return; - - // Try to remove the session from the store. - var removed = _store.TryRemove(sessionId, out var userInfo); - - // If removed, invalidate and update the list. - if (removed) + var userSessions = _activeUserSessions.Where(s => s.UserId == userId).ToList(); + foreach (var session in userSessions) { + var updatedSession = new SessionInfo(userId, userName, displayName, session.IPAddress, session.TenantId, profilePictureDataUrl, session.Status); + _activeUserSessions = _activeUserSessions.Remove(session).Add(updatedSession); using var invalidating = Invalidation.Begin(); - await GetOnlineUsers(cancellationToken).ConfigureAwait(false); + _ = await GetOnlineUsers(cancellationToken).ConfigureAwait(false); } } -} -public class UserInfoEqualityComparer : EqualityComparer -{ - /// - /// Compares two UserInfo objects for equality based on their name. - /// - /// First UserInfo object. - /// Second UserInfo object. - /// True if the names are equal, otherwise false. - public override bool Equals(UserInfo? x, UserInfo? y) + public virtual Task> GetOnlineUsers(CancellationToken cancellationToken = default) { - // Check if both references point to the same object. - if (ReferenceEquals(x, y)) return true; - - // Return false if either object is null. - if (ReferenceEquals(x, null) || ReferenceEquals(y, null)) - return false; + if (Invalidation.IsActive) + return Task.FromResult(new List()); - // Compare the names of the UserInfo objects. - return string.Equals(x.Name, y.Name, StringComparison.Ordinal); // Use string.Equals for better control. + return Task.FromResult(_activeUserSessions.ToList()); } - /// - /// Returns the hash code for a UserInfo object based on the Id. - /// - /// The UserInfo object. - /// The hash code based on the user's Id. - public override int GetHashCode(UserInfo? obj) - { - // Return 0 if the object is null. - if (ReferenceEquals(obj, null)) return 0; - // Use StringComparer to compute the hash code of the Id. - return StringComparer.Ordinal.GetHashCode(obj.Id ?? string.Empty); + + private Task GetSessionInfo() + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + throw new InvalidOperationException("HttpContext is not available."); + } + var httpUser = _httpContextAccessor.HttpContext?.User; + var ipAddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); + var userId = _httpContextAccessor.HttpContext?.User?.GetUserId(); + var userName = _httpContextAccessor.HttpContext?.User?.GetUserName(); + var displayName = _httpContextAccessor.HttpContext?.User?.GetDisplayName(); + var tenantId = _httpContextAccessor.HttpContext?.User?.GetTenantId(); + return Task.FromResult(new SessionInfo(userId, userName, displayName, ipAddress, tenantId, "", UserPresence.Available)); } + + } diff --git a/src/Server.UI/Services/Fusion/UserSessionTracker.cs b/src/Server.UI/Services/Fusion/UserSessionTracker.cs index 406d2dac0..7606fa660 100644 --- a/src/Server.UI/Services/Fusion/UserSessionTracker.cs +++ b/src/Server.UI/Services/Fusion/UserSessionTracker.cs @@ -1,57 +1,116 @@ using System.Collections.Immutable; +using System.Net.Http; using ActualLab.Fusion; +using Microsoft.AspNetCore.Http; namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; public class UserSessionTracker : IUserSessionTracker { + public UserSessionTracker(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } - private volatile ImmutableDictionary> _pageUserSessions = ImmutableDictionary>.Empty; + private volatile ImmutableDictionary> _pageUserSessions = ImmutableDictionary>.Empty; + private readonly IHttpContextAccessor _httpContextAccessor; - public async Task AddUserSession(string pageComponent, string userName, CancellationToken cancellationToken = default) + public virtual async Task AddUserSession(string pageComponent, CancellationToken cancellationToken = default) { + if (Invalidation.IsActive) return; - if (_pageUserSessions.TryGetValue(pageComponent, out var existingUsers)) - { - if (!existingUsers.Contains(userName)) - { - var updatedUsers = existingUsers.Add(userName); - _pageUserSessions = _pageUserSessions.SetItem(pageComponent, updatedUsers); - } - } - else - { - _pageUserSessions = _pageUserSessions.Add(pageComponent, ImmutableHashSet.Create(userName)); - } + var sessionInfo = await GetSessionInfo().ConfigureAwait(false); + ImmutableInterlocked.AddOrUpdate( + ref _pageUserSessions, + pageComponent, + ImmutableHashSet.Create(sessionInfo), + (key, existingSessions) => existingSessions.Add(sessionInfo)); + using var invalidating = Invalidation.Begin(); - _ = await GetUserSessions(cancellationToken).ConfigureAwait(false); - + _ = await GetUserSessions(pageComponent, cancellationToken).ConfigureAwait(false); } - public virtual Task<(string PageComponent, string[] UserSessions)[]> GetUserSessions(CancellationToken cancellationToken = default) + public virtual Task> GetUserSessions(string pageComponent,CancellationToken cancellationToken = default) { - return Task.FromResult(_pageUserSessions.Select(kvp => (kvp.Key, kvp.Value.ToArray())).ToArray()); + if (_pageUserSessions.TryGetValue(pageComponent, out var sessions)) + { + return Task.FromResult(sessions.ToList()); + } + + return Task.FromResult(new List()); } - public async Task RemoveUserSession(string pageComponent, string userName, CancellationToken cancellationToken = default) + public virtual async Task RemoveUserSession(string pageComponent, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) return; - if (_pageUserSessions.TryGetValue(pageComponent, out var users) && users.Contains(userName)) + var sessionInfo = await GetSessionInfo().ConfigureAwait(false); + + if (_pageUserSessions.TryGetValue(pageComponent, out var users) && users.Contains(sessionInfo)) { - var updatedUsers = users.Remove(userName); + var updatedUsers = users.Remove(sessionInfo); + + // Use atomic update to prevent concurrency issues if (updatedUsers.IsEmpty) { - _pageUserSessions = _pageUserSessions.Remove(pageComponent); + ImmutableInterlocked.TryRemove(ref _pageUserSessions, pageComponent, out _); } else { - _pageUserSessions = _pageUserSessions.SetItem(pageComponent, updatedUsers); + ImmutableInterlocked.AddOrUpdate( + ref _pageUserSessions, + pageComponent, + updatedUsers, + (key, existingUsers) => updatedUsers); } } using var invalidating = Invalidation.Begin(); - _ = await GetUserSessions(cancellationToken).ConfigureAwait(false); + _ = await GetUserSessions(pageComponent, cancellationToken).ConfigureAwait(false); } + + public virtual async Task RemoveAllSessions(string userId, CancellationToken cancellationToken = default) + { + if (Invalidation.IsActive) + return; + + foreach (var pageComponent in _pageUserSessions.Keys.ToList()) + { + if (_pageUserSessions.TryGetValue(pageComponent, out var users)) + { + var updatedUsers = users.Where(user => user.UserId != userId).ToImmutableHashSet(); + + // Use atomic update to prevent concurrency issues + if (updatedUsers.IsEmpty) + { + ImmutableInterlocked.TryRemove(ref _pageUserSessions, pageComponent, out _); + } + else + { + ImmutableInterlocked.AddOrUpdate( + ref _pageUserSessions, + pageComponent, + updatedUsers, + (key, existingUsers) => updatedUsers); + } + } + } + } + private Task GetSessionInfo() + { + var httpContext = _httpContextAccessor.HttpContext; + if (httpContext == null) + { + throw new InvalidOperationException("HttpContext is not available."); + } + var httpUser = _httpContextAccessor.HttpContext?.User; + var ipAddress = _httpContextAccessor.HttpContext?.Connection.RemoteIpAddress?.ToString(); + var userId = _httpContextAccessor.HttpContext?.User?.GetUserId(); + var userName = _httpContextAccessor.HttpContext?.User?.GetUserName(); + var displayName = _httpContextAccessor.HttpContext?.User?.GetDisplayName(); + var tenantId = _httpContextAccessor.HttpContext?.User?.GetTenantId(); + return Task.FromResult(new SessionInfo(userId, userName, displayName, ipAddress,tenantId,"", UserPresence.Available)); + } + } \ No newline at end of file From a0d6b4ed029511ea9f3ec71d7b52e02b733092e7 Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 1 Nov 2024 11:25:53 +0800 Subject: [PATCH 3/6] Update UserSessionTracker.cs --- src/Server.UI/Services/Fusion/UserSessionTracker.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Server.UI/Services/Fusion/UserSessionTracker.cs b/src/Server.UI/Services/Fusion/UserSessionTracker.cs index 7606fa660..9e11127f3 100644 --- a/src/Server.UI/Services/Fusion/UserSessionTracker.cs +++ b/src/Server.UI/Services/Fusion/UserSessionTracker.cs @@ -70,10 +70,10 @@ public virtual async Task RemoveUserSession(string pageComponent, CancellationTo _ = await GetUserSessions(pageComponent, cancellationToken).ConfigureAwait(false); } - public virtual async Task RemoveAllSessions(string userId, CancellationToken cancellationToken = default) + public virtual Task RemoveAllSessions(string userId, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) - return; + return Task.CompletedTask; foreach (var pageComponent in _pageUserSessions.Keys.ToList()) { @@ -96,6 +96,7 @@ public virtual async Task RemoveAllSessions(string userId, CancellationToken can } } } + return Task.CompletedTask; } private Task GetSessionInfo() { From 76344b80d361127b3c2e1ee9bb19fd214a06ab1a Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 1 Nov 2024 11:29:32 +0800 Subject: [PATCH 4/6] Update UserSessionTracker.cs --- src/Server.UI/Services/Fusion/UserSessionTracker.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Server.UI/Services/Fusion/UserSessionTracker.cs b/src/Server.UI/Services/Fusion/UserSessionTracker.cs index 9e11127f3..9785474c4 100644 --- a/src/Server.UI/Services/Fusion/UserSessionTracker.cs +++ b/src/Server.UI/Services/Fusion/UserSessionTracker.cs @@ -1,7 +1,5 @@ using System.Collections.Immutable; -using System.Net.Http; using ActualLab.Fusion; -using Microsoft.AspNetCore.Http; namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; From 4daff2d87a952e208d1373b6acc10bec094db68b Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 1 Nov 2024 13:26:08 +0800 Subject: [PATCH 5/6] add comments --- .../Middlewares/UserSessionCircuitHandler.cs | 27 +++++++-- .../Services/Fusion/OnlineUserTracker.cs | 55 ++++++++++++++---- .../Services/Fusion/UserSessionTracker.cs | 56 +++++++++++++++---- 3 files changed, 113 insertions(+), 25 deletions(-) diff --git a/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs b/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs index c00b583a8..c57318a03 100644 --- a/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs +++ b/src/Server.UI/Middlewares/UserSessionCircuitHandler.cs @@ -3,26 +3,45 @@ namespace CleanArchitecture.Blazor.Server.UI.Middlewares; +/// +/// Handles user session tracking and online user tracking for Blazor server circuits. +/// public class UserSessionCircuitHandler : CircuitHandler { private readonly IUserSessionTracker _userSessionTracker; private readonly IOnlineUserTracker _onlineUserTracker; private readonly IHttpContextAccessor _httpContextAccessor; - - public UserSessionCircuitHandler(IUserSessionTracker userSessionTracker, IOnlineUserTracker OnlineUserTracker, IHttpContextAccessor httpContextAccessor) + /// + /// Initializes a new instance of the class. + /// + /// The user session tracker service. + /// The online user tracker service. + /// The HTTP context accessor. + public UserSessionCircuitHandler(IUserSessionTracker userSessionTracker, IOnlineUserTracker onlineUserTracker, IHttpContextAccessor httpContextAccessor) { _userSessionTracker = userSessionTracker; - _onlineUserTracker = OnlineUserTracker; + _onlineUserTracker = onlineUserTracker; _httpContextAccessor = httpContextAccessor; - } + /// + /// Called when a new circuit connection is established. + /// + /// The circuit. + /// The cancellation token. + /// A task that represents the asynchronous operation. public override async Task OnConnectionUpAsync(Circuit circuit, CancellationToken cancellationToken) { await base.OnConnectionUpAsync(circuit, cancellationToken); } + /// + /// Called when a circuit connection is disconnected. + /// + /// The circuit. + /// The cancellation token. + /// A task that represents the asynchronous operation. public override async Task OnConnectionDownAsync(Circuit circuit, CancellationToken cancellationToken) { var userId = _httpContextAccessor.HttpContext?.User.GetUserId(); diff --git a/src/Server.UI/Services/Fusion/OnlineUserTracker.cs b/src/Server.UI/Services/Fusion/OnlineUserTracker.cs index e57f3671f..5ab0f6913 100644 --- a/src/Server.UI/Services/Fusion/OnlineUserTracker.cs +++ b/src/Server.UI/Services/Fusion/OnlineUserTracker.cs @@ -5,21 +5,33 @@ namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; +/// +/// Tracks online users and manages their sessions. +/// public class OnlineUserTracker : IOnlineUserTracker { + private volatile ImmutableHashSet _activeUserSessions = ImmutableHashSet.Empty; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP context accessor. public OnlineUserTracker(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } - private volatile ImmutableHashSet _activeUserSessions = ImmutableHashSet.Empty; - private readonly IHttpContextAccessor _httpContextAccessor; - - public virtual async Task Initial( SessionInfo sessionInfo, CancellationToken cancellationToken = default) + /// + /// Initializes the session for a user. + /// + /// The session information. + /// The cancellation token. + public virtual async Task Initial(SessionInfo sessionInfo, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) return; - if (!_activeUserSessions.Any(x=>x.UserId==sessionInfo.UserId)) + if (!_activeUserSessions.Any(x => x.UserId == sessionInfo.UserId)) { _activeUserSessions = _activeUserSessions.Add(sessionInfo); using var invalidating = Invalidation.Begin(); @@ -27,6 +39,10 @@ public virtual async Task Initial( SessionInfo sessionInfo, CancellationToken ca } } + /// + /// Logs out the current user. + /// + /// The cancellation token. public virtual async Task Logout(CancellationToken cancellationToken = default) { if (Invalidation.IsActive) @@ -41,6 +57,11 @@ public virtual async Task Logout(CancellationToken cancellationToken = default) } } + /// + /// Clears all sessions for a specific user. + /// + /// The user ID. + /// The cancellation token. public virtual async Task Clear(string userId, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) @@ -54,6 +75,14 @@ public virtual async Task Clear(string userId, CancellationToken cancellationTok } } + /// + /// Updates the session information for a specific user. + /// + /// The user ID. + /// The user name. + /// The display name. + /// The profile picture data URL. + /// The cancellation token. public virtual async Task Update(string userId, string userName, string displayName, string profilePictureDataUrl, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) @@ -61,13 +90,18 @@ public virtual async Task Update(string userId, string userName, string displayN var userSessions = _activeUserSessions.Where(s => s.UserId == userId).ToList(); foreach (var session in userSessions) { - var updatedSession = new SessionInfo(userId, userName, displayName, session.IPAddress, session.TenantId, profilePictureDataUrl, session.Status); + var updatedSession = new SessionInfo(userId, userName, displayName, session.IPAddress, session.TenantId, profilePictureDataUrl, session.Status); _activeUserSessions = _activeUserSessions.Remove(session).Add(updatedSession); using var invalidating = Invalidation.Begin(); _ = await GetOnlineUsers(cancellationToken).ConfigureAwait(false); } } + /// + /// Gets the list of online users. + /// + /// The cancellation token. + /// A list of online user sessions. public virtual Task> GetOnlineUsers(CancellationToken cancellationToken = default) { if (Invalidation.IsActive) @@ -76,8 +110,11 @@ public virtual Task> GetOnlineUsers(CancellationToken cancella return Task.FromResult(_activeUserSessions.ToList()); } - - + /// + /// Gets the session information for the current user. + /// + /// The session information. + /// Thrown when the HTTP context is not available. private Task GetSessionInfo() { var httpContext = _httpContextAccessor.HttpContext; @@ -93,6 +130,4 @@ private Task GetSessionInfo() var tenantId = _httpContextAccessor.HttpContext?.User?.GetTenantId(); return Task.FromResult(new SessionInfo(userId, userName, displayName, ipAddress, tenantId, "", UserPresence.Available)); } - - } diff --git a/src/Server.UI/Services/Fusion/UserSessionTracker.cs b/src/Server.UI/Services/Fusion/UserSessionTracker.cs index 9785474c4..2e6d8c5c5 100644 --- a/src/Server.UI/Services/Fusion/UserSessionTracker.cs +++ b/src/Server.UI/Services/Fusion/UserSessionTracker.cs @@ -3,21 +3,33 @@ namespace CleanArchitecture.Blazor.Server.UI.Services.Fusion; +/// +/// Tracks user sessions for different page components. +/// public class UserSessionTracker : IUserSessionTracker { - public UserSessionTracker(IHttpContextAccessor httpContextAccessor) + private volatile ImmutableDictionary> _pageUserSessions = ImmutableDictionary>.Empty; + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP context accessor. + public UserSessionTracker(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } - private volatile ImmutableDictionary> _pageUserSessions = ImmutableDictionary>.Empty; - private readonly IHttpContextAccessor _httpContextAccessor; - + /// + /// Adds a user session for the specified page component. + /// + /// The page component. + /// The cancellation token. public virtual async Task AddUserSession(string pageComponent, CancellationToken cancellationToken = default) { - if (Invalidation.IsActive) return; + var sessionInfo = await GetSessionInfo().ConfigureAwait(false); ImmutableInterlocked.AddOrUpdate( ref _pageUserSessions, @@ -29,7 +41,13 @@ public virtual async Task AddUserSession(string pageComponent, CancellationToken _ = await GetUserSessions(pageComponent, cancellationToken).ConfigureAwait(false); } - public virtual Task> GetUserSessions(string pageComponent,CancellationToken cancellationToken = default) + /// + /// Gets the user sessions for the specified page component. + /// + /// The page component. + /// The cancellation token. + /// A list of session information. + public virtual Task> GetUserSessions(string pageComponent, CancellationToken cancellationToken = default) { if (_pageUserSessions.TryGetValue(pageComponent, out var sessions)) { @@ -39,10 +57,16 @@ public virtual Task> GetUserSessions(string pageComponent,Can return Task.FromResult(new List()); } + /// + /// Removes a user session for the specified page component. + /// + /// The page component. + /// The cancellation token. public virtual async Task RemoveUserSession(string pageComponent, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) return; + var sessionInfo = await GetSessionInfo().ConfigureAwait(false); if (_pageUserSessions.TryGetValue(pageComponent, out var users) && users.Contains(sessionInfo)) @@ -68,7 +92,12 @@ public virtual async Task RemoveUserSession(string pageComponent, CancellationTo _ = await GetUserSessions(pageComponent, cancellationToken).ConfigureAwait(false); } - public virtual Task RemoveAllSessions(string userId, CancellationToken cancellationToken = default) + /// + /// Removes all sessions for the specified user. + /// + /// The user identifier. + /// The cancellation token. + public virtual Task RemoveAllSessions(string userId, CancellationToken cancellationToken = default) { if (Invalidation.IsActive) return Task.CompletedTask; @@ -96,7 +125,13 @@ public virtual Task RemoveAllSessions(string userId, CancellationToken cancella } return Task.CompletedTask; } - private Task GetSessionInfo() + + /// + /// Gets the session information from the current HTTP context. + /// + /// The session information. + /// Thrown when the HTTP context is not available. + private Task GetSessionInfo() { var httpContext = _httpContextAccessor.HttpContext; if (httpContext == null) @@ -109,7 +144,6 @@ private Task GetSessionInfo() var userName = _httpContextAccessor.HttpContext?.User?.GetUserName(); var displayName = _httpContextAccessor.HttpContext?.User?.GetDisplayName(); var tenantId = _httpContextAccessor.HttpContext?.User?.GetTenantId(); - return Task.FromResult(new SessionInfo(userId, userName, displayName, ipAddress,tenantId,"", UserPresence.Available)); + return Task.FromResult(new SessionInfo(userId, userName, displayName, ipAddress, tenantId, "", UserPresence.Available)); } - -} \ No newline at end of file +} From 991f6d164dcc7e230055da9f023e2a2786ee23e6 Mon Sep 17 00:00:00 2001 From: hualin Date: Fri, 1 Nov 2024 14:07:39 +0800 Subject: [PATCH 6/6] clear Variant --- .../Components/Breadcrumbs/Breadcrumbs.razor | 4 +-- .../Dialogs/ConfirmationDialog.razor | 4 +-- .../Dialogs/DeleteConfirmation.razor | 4 +-- .../Dialogs/LogoutConfirmation.razor | 4 +-- src/Server.UI/Components/Routes.razor | 2 +- .../Components/Shared/CustomError.razor | 2 +- src/Server.UI/DependencyInjection.cs | 2 ++ src/Server.UI/Pages/Contacts/Contacts.razor | 6 ++--- .../Pages/Contacts/CreateContact.razor | 2 +- .../Pages/Contacts/EditContact.razor | 2 +- src/Server.UI/Pages/Documents/Documents.razor | 6 ++--- .../Pages/Identity/Roles/Roles.razor | 6 ++--- .../Identity/Users/Components/UserForm.razor | 22 ++++++++-------- .../Pages/Identity/Users/Profile.razor | 26 +++++++++---------- .../Pages/Identity/Users/Users.razor | 6 ++--- .../Pages/PicklistSets/PicklistSets.razor | 6 ++--- src/Server.UI/Pages/Products/Products.razor | 6 ++--- .../Pages/SystemManagement/AuditTrails.razor | 2 +- .../Pages/SystemManagement/Logs.razor | 4 +-- src/Server.UI/Pages/Tenants/Tenants.razor | 6 ++--- 20 files changed, 62 insertions(+), 60 deletions(-) diff --git a/src/Server.UI/Components/Breadcrumbs/Breadcrumbs.razor b/src/Server.UI/Components/Breadcrumbs/Breadcrumbs.razor index 36cb4edf8..2938c944c 100644 --- a/src/Server.UI/Components/Breadcrumbs/Breadcrumbs.razor +++ b/src/Server.UI/Components/Breadcrumbs/Breadcrumbs.razor @@ -8,11 +8,11 @@ @if (OnSaveButtonClick.HasDelegate) { - @ConstantString.Save + @ConstantString.Save } @if (OnGoEditClick.HasDelegate) { - @ConstantString.Edit + @ConstantString.Edit } @if (OnDeleteClick.HasDelegate || OnPrintClick.HasDelegate) { diff --git a/src/Server.UI/Components/Dialogs/ConfirmationDialog.razor b/src/Server.UI/Components/Dialogs/ConfirmationDialog.razor index 1e0924640..2ecdefc9e 100644 --- a/src/Server.UI/Components/Dialogs/ConfirmationDialog.razor +++ b/src/Server.UI/Components/Dialogs/ConfirmationDialog.razor @@ -5,8 +5,8 @@ @ContentText - @ConstantString.Cancel - @ConstantString.Confirm + @ConstantString.Cancel + @ConstantString.Confirm diff --git a/src/Server.UI/Components/Dialogs/DeleteConfirmation.razor b/src/Server.UI/Components/Dialogs/DeleteConfirmation.razor index b61713947..15d79c67f 100644 --- a/src/Server.UI/Components/Dialogs/DeleteConfirmation.razor +++ b/src/Server.UI/Components/Dialogs/DeleteConfirmation.razor @@ -11,8 +11,8 @@ @ContentText - @ConstantString.Cancel - @ConstantString.Confirm + @ConstantString.Cancel + @ConstantString.Confirm diff --git a/src/Server.UI/Components/Dialogs/LogoutConfirmation.razor b/src/Server.UI/Components/Dialogs/LogoutConfirmation.razor index 4a049dcc4..a7f88d49a 100644 --- a/src/Server.UI/Components/Dialogs/LogoutConfirmation.razor +++ b/src/Server.UI/Components/Dialogs/LogoutConfirmation.razor @@ -9,8 +9,8 @@ @ContentText - @ConstantString.Cancel - @ConstantString.Logout + @ConstantString.Cancel + @ConstantString.Logout diff --git a/src/Server.UI/Components/Routes.razor b/src/Server.UI/Components/Routes.razor index eec12d070..4ba8f736a 100644 --- a/src/Server.UI/Components/Routes.razor +++ b/src/Server.UI/Components/Routes.razor @@ -16,7 +16,7 @@ @L["You are not authorized to view this page."] @L["If you need access to this page, please contact the administrator."] - @L["Contact Administrator"] + @L["Contact Administrator"] diff --git a/src/Server.UI/Components/Shared/CustomError.razor b/src/Server.UI/Components/Shared/CustomError.razor index d7bc591a8..537985e4d 100644 --- a/src/Server.UI/Components/Shared/CustomError.razor +++ b/src/Server.UI/Components/Shared/CustomError.razor @@ -24,7 +24,7 @@
- @ConstantString.Refresh + @ConstantString.Refresh diff --git a/src/Server.UI/DependencyInjection.cs b/src/Server.UI/DependencyInjection.cs index 0557d8a22..c9f6eaefa 100644 --- a/src/Server.UI/DependencyInjection.cs +++ b/src/Server.UI/DependencyInjection.cs @@ -44,6 +44,8 @@ public static IServiceCollection AddServerUI(this IServiceCollection services, I services.AddMudServices(config => { MudGlobal.InputDefaults.ShrinkLabel = true; + //MudGlobal.InputDefaults.Variant = Variant.Outlined; + //MudGlobal.ButtonDefaults.Variant = Variant.Outlined; config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter; config.SnackbarConfiguration.NewestOnTop = false; config.SnackbarConfiguration.ShowCloseIcon = true; diff --git a/src/Server.UI/Pages/Contacts/Contacts.razor b/src/Server.UI/Pages/Contacts/Contacts.razor index 90cddc16c..256e1195f 100644 --- a/src/Server.UI/Pages/Contacts/Contacts.razor +++ b/src/Server.UI/Pages/Contacts/Contacts.razor @@ -46,7 +46,7 @@ - @@ -54,13 +54,13 @@ @if (_canCreate) { - @ConstantString.New } - + @if (_canCreate) { @ConstantString.Clone diff --git a/src/Server.UI/Pages/Contacts/CreateContact.razor b/src/Server.UI/Pages/Contacts/CreateContact.razor index 84b635eb5..8ff0d81b6 100644 --- a/src/Server.UI/Pages/Contacts/CreateContact.razor +++ b/src/Server.UI/Pages/Contacts/CreateContact.razor @@ -37,7 +37,7 @@ - @ConstantString.Save + @ConstantString.Save diff --git a/src/Server.UI/Pages/Contacts/EditContact.razor b/src/Server.UI/Pages/Contacts/EditContact.razor index 246c5b3fe..f4fa9f0d6 100644 --- a/src/Server.UI/Pages/Contacts/EditContact.razor +++ b/src/Server.UI/Pages/Contacts/EditContact.razor @@ -42,7 +42,7 @@ - @ConstantString.Save + @ConstantString.Save } diff --git a/src/Server.UI/Pages/Documents/Documents.razor b/src/Server.UI/Pages/Documents/Documents.razor index d92a34fda..eb0eac7cc 100644 --- a/src/Server.UI/Pages/Documents/Documents.razor +++ b/src/Server.UI/Pages/Documents/Documents.razor @@ -41,7 +41,7 @@ - @@ -49,7 +49,7 @@ @if (_canCreate) { - @L["Upload Pictures"] @@ -57,7 +57,7 @@ } @if (_canDelete) { - diff --git a/src/Server.UI/Pages/Identity/Roles/Roles.razor b/src/Server.UI/Pages/Identity/Roles/Roles.razor index 6cb8a7f7f..669f348e7 100644 --- a/src/Server.UI/Pages/Identity/Roles/Roles.razor +++ b/src/Server.UI/Pages/Identity/Roles/Roles.razor @@ -44,7 +44,7 @@ - @@ -52,13 +52,13 @@ @if (_canCreate) { - @ConstantString.New } - + @if (_canDelete) { - + - +
@@ -67,7 +67,7 @@
- + - + - + - + - + @@ -114,16 +114,16 @@ - + - + - + - + diff --git a/src/Server.UI/Pages/Identity/Users/Profile.razor b/src/Server.UI/Pages/Identity/Users/Profile.razor index f6294a163..1c54a6d40 100644 --- a/src/Server.UI/Pages/Identity/Users/Profile.razor +++ b/src/Server.UI/Pages/Identity/Users/Profile.razor @@ -68,31 +68,31 @@ else - + - + - + - + - + - + - + - + - + @if (_submitting) { @@ -115,7 +115,7 @@ else Label="@L["Current Password"]" For="@(() => _changepassword.CurrentPassword)" @bind-Value="_changepassword.CurrentPassword" - Variant="Variant.Text" + Required="true" /> @@ -123,7 +123,7 @@ else Label="@L["New Password"]" For="@(() => _changepassword.NewPassword)" @bind-Value="_changepassword.NewPassword" - Variant="Variant.Text" + Required="true" /> @@ -131,11 +131,11 @@ else Label="@L["Confirm New Password"]" For="@(() => _changepassword.ConfirmPassword)" @bind-Value="_changepassword.ConfirmPassword" - Variant="Variant.Text" + Required="true" /> - + @if (_submitting) { diff --git a/src/Server.UI/Pages/Identity/Users/Users.razor b/src/Server.UI/Pages/Identity/Users/Users.razor index d8ef6f8a2..277f6a195 100644 --- a/src/Server.UI/Pages/Identity/Users/Users.razor +++ b/src/Server.UI/Pages/Identity/Users/Users.razor @@ -66,7 +66,7 @@
- @@ -74,13 +74,13 @@ @if (_canCreate) { - @ConstantString.New } - + @if (_canDelete) { - @@ -49,13 +49,13 @@ @if (_canCreate) { - @ConstantString.New } - + @if (_canDelete) { - @@ -50,13 +50,13 @@ @if (_canCreate) { - @ConstantString.New } - + @if (_canCreate) { @ConstantString.Clone diff --git a/src/Server.UI/Pages/SystemManagement/AuditTrails.razor b/src/Server.UI/Pages/SystemManagement/AuditTrails.razor index 95d5c9b79..b71ab400d 100644 --- a/src/Server.UI/Pages/SystemManagement/AuditTrails.razor +++ b/src/Server.UI/Pages/SystemManagement/AuditTrails.razor @@ -31,7 +31,7 @@ - @ConstantString.Refresh diff --git a/src/Server.UI/Pages/SystemManagement/Logs.razor b/src/Server.UI/Pages/SystemManagement/Logs.razor index 32a0a3e71..45c12f6e0 100644 --- a/src/Server.UI/Pages/SystemManagement/Logs.razor +++ b/src/Server.UI/Pages/SystemManagement/Logs.razor @@ -35,14 +35,14 @@ - @ConstantString.Refresh @if (_canPurge) { - diff --git a/src/Server.UI/Pages/Tenants/Tenants.razor b/src/Server.UI/Pages/Tenants/Tenants.razor index 45f1ab2c1..c82d2d9c5 100644 --- a/src/Server.UI/Pages/Tenants/Tenants.razor +++ b/src/Server.UI/Pages/Tenants/Tenants.razor @@ -31,7 +31,7 @@ - @@ -39,7 +39,7 @@ @if (_canCreate) { - @ConstantString.New @@ -47,7 +47,7 @@ } @if (_canDelete) { -