- @if (Users == null)
- {
-
Fetching Data...
- }
- else
- foreach (User user in Users)
- {
-
-
-
-
-
-
- Ratio
-
-
+
+
+
+
+
+
+
+
+
+ Cancel
+ Add Account
+
+
+
+
+
+
+
+
+
+ Add Account
- }
+
+
+ Remove Selection
+
+
+ Accounts
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Is Administrator :
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Pages/UserListPanel.razor.cs b/Pages/UserListPanel.razor.cs
index 243fb9f..f4eff47 100644
--- a/Pages/UserListPanel.razor.cs
+++ b/Pages/UserListPanel.razor.cs
@@ -1,34 +1,121 @@
-using System.Net.Http.Json;
+using System.Text;
using AdminPanel.Models;
+using AdminPanel.Services;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.Extensions.Primitives;
+using MudBlazor;
namespace AdminPanel.Pages
{
public partial class UserListPanel
{
- public List? Users { get; private set; }
+ [Inject] private ISnackbar Snackbar { get; init; }
+ [Inject] private IUsersService Service { get; init; }
+ [Inject] private ILogger Logger { get; init; }
- public UserListPanel()
+ private MudDataGrid Grid { get; set; }
+
+
+ private string? SearchString { get; set; }
+
+
+ private HashSet SelectedItems { get; set; } = new();
+
+ private string? FormAccountName { get; set; }
+ private string? FormAccountEmail { get; set; }
+ private string? FormAccountPassword { get; set; }
+ private bool FormAccountIsAdmin { get; set; }
+
+ public bool IsAddingUser { get; set; }
+
+
+ private async Task> LoadUsersFromServer(GridState state)
{
- HttpClient client = new()
+ Logger.LogDebug($"Loading users from server, state = {state} searchString = {SearchString}");
+ var (count, users) =
+ await Service.ListUsers((uint)(state.Page * state.PageSize), (uint)state.PageSize, SearchString);
+ return new GridData
{
- BaseAddress = new Uri("http://localhost:8080")
+ TotalItems = (int)count,
+ Items = users
};
- RetrieveUsers(client);
}
- public void OnAccessToUserSpaceClicked(User user)
+
+ private async void OnAccountUpdated(User user)
+ {
+ Logger.LogDebug($"Account updated : {user}");
+ try
+ {
+ await Service.UpdateUser(user);
+ }
+ catch (ServiceException err)
+ {
+ ShowErrors(err);
+ }
+ }
+
+ private async void ConfirmAddAccount(MouseEventArgs e)
{
+ // We no longer add an account if it is confirmed
+ IsAddingUser = false;
+
+ try
+ {
+ var user = await Service.AddUser(FormAccountName!, FormAccountEmail!, FormAccountPassword!,
+ FormAccountIsAdmin);
+ Logger.LogDebug($"Added user : {user}");
+ await Grid.ReloadServerData();
+ }
+ catch (ServiceException err)
+ {
+ ShowErrors(err);
+ }
+ }
+
+ private void ShowErrors(ServiceException e)
+ {
+ foreach (var erronedArgument in e.ArgumentMessages)
+ {
+ var sb = new StringBuilder(erronedArgument.Key);
+ sb.Append(" :");
+
+ foreach (var message in erronedArgument.Value)
+ {
+ sb.Append("\n\t-");
+ sb.Append(message);
+ }
+ Snackbar.Add(sb.ToString());
+ }
}
- private async void RetrieveUsers(HttpClient client)
+ private async void RemoveSelection(MouseEventArgs e)
{
- using HttpResponseMessage response = await client.GetAsync("/api/admin/list-users?start=0&n=100");
+ var users = SelectedItems.ToList().ConvertAll(u => u.Id);
+ Logger.LogDebug($"Removing users : {users}");
+ try
+ {
+ await Service.RemoveUsers(users);
+ await Grid.ReloadServerData();
+ }
+ catch (ServiceException err)
+ {
+ ShowErrors(err);
+ }
+ }
- response.EnsureSuccessStatusCode();
+ private Func VerifyLength(uint min, uint max)
+ {
+ return s => s.Length >= min && s.Length <= max
+ ? null
+ : $"length is incorrect (must be between {min} and {max})";
+ }
- Users = (await response.Content.ReadFromJsonAsync>())!;
- StateHasChanged();
- Console.WriteLine(Users);
+ private void ValidateSearch(KeyboardEventArgs e)
+ {
+ Grid.ReloadServerData();
+ Logger.LogDebug($"Searching for {SearchString}");
}
}
}
\ No newline at end of file
diff --git a/Pages/UserListPanel.razor.css b/Pages/UserListPanel.razor.css
new file mode 100644
index 0000000..d29687f
--- /dev/null
+++ b/Pages/UserListPanel.razor.css
@@ -0,0 +1,8 @@
+
+
+#add-account-form {
+ background-color: white;
+ visibility: hidden;
+ color: #ffba00;
+}
+
diff --git a/Program.cs b/Program.cs
index d4e1597..91dd8e2 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,11 +1,28 @@
using AdminPanel;
+using AdminPanel.Pages;
+using AdminPanel.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add("#app");
builder.RootComponents.Add("head::after");
-builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
+builder.Services
+ .AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
-await builder.Build().RunAsync();
+// builder.Logging.SetMinimumLevel(LogLevel.Debug);
+
+var client = new HttpClient
+{
+ BaseAddress = new Uri("http://localhost:8080")
+};
+
+builder.Services.AddScoped(sp => new HttpUsersService(client));
+//builder.Services.AddScoped();
+
+builder.Services.AddMudServices();
+
+
+await builder.Build().RunAsync();
\ No newline at end of file
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
index db1c6be..d5df975 100644
--- a/Properties/launchSettings.json
+++ b/Properties/launchSettings.json
@@ -2,8 +2,8 @@
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
- "iisExpress": {
- "applicationUrl": "http://localhost:18284",
+ "iisExpress": {
+ "applicationUrl": "http://localhost:18284",
"sslPort": 0
}
},
@@ -12,8 +12,8 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
- "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
- "applicationUrl": "http://localhost:5081",
+ "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
+ "applicationUrl": "http://localhost:5081",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/Services/HttpUsersService.cs b/Services/HttpUsersService.cs
new file mode 100644
index 0000000..824fefd
--- /dev/null
+++ b/Services/HttpUsersService.cs
@@ -0,0 +1,93 @@
+using System.Diagnostics;
+using System.Net;
+using System.Net.Http.Json;
+using AdminPanel.Models;
+
+namespace AdminPanel.Services;
+
+public class HttpUsersService : IUsersService
+{
+ private HttpClient Client { get; }
+
+ public HttpUsersService(HttpClient client)
+ {
+ this.Client = client;
+ }
+
+ private record ServerErrorMessageDto(string? Field, string? Error, string Message);
+
+ private async Task EnsureResponseIsOk(HttpResponseMessage response)
+ {
+
+ if (response.StatusCode == HttpStatusCode.OK)
+ {
+ return;
+ }
+
+ var content = await response.Content.ReadFromJsonAsync();
+ var messages = content!
+ .GroupBy(e => e.Field ?? e.Error!)
+ .ToDictionary(
+ g => g.Key,
+ g => g.ToList().ConvertAll(e => e.Message)
+ );
+
+ throw new ServiceException("Server refused request", messages);
+ }
+
+ private record ListUsersResponseDto(List Users, uint TotalCount);
+
+ public async Task<(uint, List)> ListUsers(uint from, uint len, string? searchString = null)
+ {
+ var httpResponse =
+ await Client.GetAsync($"/api/admin/list-users?start={from}&n={len}&search={searchString ?? ""}");
+ await EnsureResponseIsOk(httpResponse);
+
+ var response = await httpResponse.Content.ReadFromJsonAsync()
+ ?? throw new Exception("Received non-parseable json from server");
+
+ return (response.TotalCount, response.Users);
+ }
+
+ private record AddUserRequestDto(string Username, string Email, string Password, bool IsAdmin);
+
+ private record AddUserResponseDto(uint Id);
+
+ public async Task AddUser(string username, string email, string password, bool isAdmin)
+ {
+ var httpResponse = await Client.PostAsJsonAsync("/api/admin/user/add",
+ new AddUserRequestDto(username, email, password, isAdmin));
+
+ await EnsureResponseIsOk(httpResponse);
+
+ var response = await httpResponse.Content.ReadFromJsonAsync()
+ ?? throw new Exception("Received non-parseable json from server");
+
+ return new User
+ {
+ Name = username,
+ Email = email,
+ IsAdmin = isAdmin,
+ Id = response.Id
+ };
+ }
+
+ private record RemoveUsersRequestDto(uint[] Identifiers);
+
+ public async Task RemoveUsers(IEnumerable userIds)
+ {
+ var httpResponse =
+ await Client.PostAsJsonAsync("/api/admin/user/remove-all", new RemoveUsersRequestDto(userIds.ToArray()));
+
+ await EnsureResponseIsOk(httpResponse);
+ }
+
+ private record UpdateUserRequestDto(string Email, string Username, bool IsAdmin);
+
+ public async Task UpdateUser(User user)
+ {
+ var httpResponse = await Client.PostAsJsonAsync($"/api/admin/user/{user.Id}/update",
+ new UpdateUserRequestDto(user.Email, user.Name, user.IsAdmin));
+ await EnsureResponseIsOk(httpResponse);
+ }
+}
\ No newline at end of file
diff --git a/Services/IUsersService.cs b/Services/IUsersService.cs
new file mode 100644
index 0000000..c523caf
--- /dev/null
+++ b/Services/IUsersService.cs
@@ -0,0 +1,15 @@
+using AdminPanel.Models;
+
+namespace AdminPanel.Services
+{
+ public interface IUsersService
+ {
+ public Task<(uint, List)> ListUsers(uint from, uint len, string? searchString = null);
+
+ public Task AddUser(string username, string email, string password, bool isAdmin);
+
+ public Task RemoveUsers(IEnumerable userId);
+
+ public Task UpdateUser(User user);
+ }
+}
diff --git a/Services/ServiceException.cs b/Services/ServiceException.cs
new file mode 100644
index 0000000..8dbcb00
--- /dev/null
+++ b/Services/ServiceException.cs
@@ -0,0 +1,18 @@
+namespace AdminPanel.Services;
+
+class ServiceException : Exception
+{
+ public Dictionary> ArgumentMessages { get; init; }
+
+
+
+ public ServiceException(string? message, Dictionary> arguments) : base(message)
+ {
+ ArgumentMessages = arguments;
+ }
+
+ public ServiceException(string? message, Dictionary> arguments, Exception? innerException) : base(message, innerException)
+ {
+ ArgumentMessages = arguments;
+ }
+}
\ No newline at end of file
diff --git a/Services/UsersServiceStub.cs b/Services/UsersServiceStub.cs
new file mode 100644
index 0000000..d30845e
--- /dev/null
+++ b/Services/UsersServiceStub.cs
@@ -0,0 +1,65 @@
+using AdminPanel.Models;
+
+namespace AdminPanel.Services
+{
+ public class UsersServiceStub : IUsersService
+ {
+ private Dictionary Users { get; } = new[]
+ {
+ new User
+ {
+ Name = "Mathis",
+ Email = "mathis@gmail.com",
+ Id = 0,
+ IsAdmin = true
+ },
+ new User
+ {
+ Name = "Maeva",
+ Email = "maeva@gmail.com",
+ Id = 1,
+ IsAdmin = false
+ },
+ }.ToDictionary(u => u.Id);
+
+ public Task AddUser(string username, string email, string password, bool isAdmin)
+ {
+ User user = new User
+ {
+ Email = email,
+ Name = username,
+ IsAdmin = isAdmin,
+ Id = (uint) Users.Count
+ };
+ Users[user.Id] = user;
+ return Task.FromResult(user);
+ }
+
+ public async Task<(uint, List)> ListUsers(uint from, uint len, string? searchString = null)
+ {
+ //simulate a 1sec long request
+ await Task.Delay(1000);
+ var slice = Users.Values
+ .ToList()
+ .FindAll(a => searchString == null || a.Name.Contains(searchString) || a.Email.Contains(searchString))
+ .GetRange((int)from, (int)(from + len > Users.Count ? Users.Count - from : len));
+
+ return ((uint)Users.Count, slice);
+ }
+
+ public Task RemoveUsers(IEnumerable userIds)
+ {
+ foreach (var id in userIds)
+ {
+ Users.Remove(id);
+ }
+ return Task.CompletedTask;
+ }
+
+ public Task UpdateUser(User user)
+ {
+ Users[user.Id] = user;
+ return Task.CompletedTask;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Shared/MainLayout.razor b/Shared/MainLayout.razor
index 0b3cad5..dc88df5 100644
--- a/Shared/MainLayout.razor
+++ b/Shared/MainLayout.razor
@@ -1,4 +1,8 @@
@inherits LayoutComponentBase
+@using MudBlazor
+
+
+