Compare commits

...

5 Commits

Author SHA1 Message Date
maxime.batista 4256234e27 add readme
1 year ago
maxime 6c351dfd99 update routes
1 year ago
sam 97ecfbd51d Additions of some finishes
1 year ago
Samuel BERION 21404dc813 Add teams administration panel (#3)
1 year ago
Maxime BATISTA a295a7c1ff Merge pull request 'Add Users And account administration panel' (#1) from users into master
1 year ago

@ -18,6 +18,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazorise.Bootstrap" Version="1.4.0" />
<PackageReference Include="Blazorise.DataGrid" Version="1.4.0" />
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.4.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.13" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.13" PrivateAssets="all" />
<PackageReference Include="MudBlazor" Version="6.12.0" />
@ -27,4 +30,12 @@
<ServiceWorker Include="wwwroot\service-worker.js" PublishedContent="wwwroot\service-worker.published.js" />
</ItemGroup>
<ItemGroup>
<Content Remove="Pages\TeamListPanel.razor.cs.razor" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Components\TeamComponents.cs" />
</ItemGroup>
</Project>

@ -1,5 +1,4 @@
<div>
@if (@User.IsAdmin)
{
<h3>Administrator @User.Name</h3>
@ -8,6 +7,7 @@
{
<h3>@User.Name</h3>
}
<p>email: @User.Email</p>
<p>id: @User.Id</p>
</div>

@ -0,0 +1,6 @@
namespace AdminPanel.Models;
public record Team(uint Id, string Name, string Picture, string MainColor, string SecondColor)
{
}

@ -0,0 +1,24 @@
using System.Text;
using AdminPanel.Services;
using MudBlazor;
namespace AdminPanel.Pages;
public class DisplayUtils
{
public static void ShowErrors(ServiceException e, ISnackbar snackbar)
{
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());
}
}
}

@ -1,10 +1,106 @@
@page "/teams"
@using AdminPanel.Models
@using DataGridEditMode = MudBlazor.DataGridEditMode
<PageTitle>Teams Panel</PageTitle>
<h3>TeamListPanel</h3>
<h1>Team Panel</h1>
@code {
}
<MudPopover Open="@_isOpenAdd" Fixed="true" Class="px-4 pt-4">
<div class="d-flex flex-column">
<MudForm @bind-IsValid="@_success" @bind-Errors="@_errors">
<MudTextField
T="string"
Label="Name"
Required="true"
@bind-Value="FormName"
RequiredError="Team's name is required!" />
<MudTextField
T="string"
Label="Picture"
Required="true"
@bind-Value="FormPicture"
RequiredError="Picture is required!"/>
<MudTextField
T="string"
Label="MainColor"
Required="true"
@bind-Value="FormMainColor"
RequiredError="Main color is required!"/>
<MudTextField
T="string"
Label="SecondaryColor"
Required="true"
@bind-Value="FormSecondaryColor"
RequiredError="Secondary color is required!"/>
<div class="d-flex justify-center">
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@AddTeamConfirmed" Class="ml-auto">Apply</MudButton>
</div>
</MudForm>
<MudButton OnClick="@ToggleOpen" Class="ml-auto mr-n3 mb-1" Color="Color.Error">Close</MudButton>
</div>
</MudPopover>
<MudCard>
<ToolBarContent style="display: flex; justify-content: space-evenly;">
<!--<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@ToggleOpen">Ajouter</MudButton>-->
<div style="display: flex; align-items: center">
<MudIconButton Icon="@Icons.Material.Filled.Add" OnClick="@ToggleOpen" Color="@Color.Success"/>
<MudText>Add Team</MudText>
</div>
<div style="display: flex; align-items: center">
<MudIconButton Icon="@Icons.Material.Filled.Remove" OnClick="@ToggleOpenConfirmation" Color="@Color.Error"/>
<MudText>Remove selection</MudText>
</div>
<!--<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@ToggleOpenConfirmation">Supprimer</MudButton>-->
<MudSpacer/>
<MudText>Teams</MudText>
<MudSpacer/>
</ToolBarContent>
</MudCard>
<MudPopover Open="@_isOpenConf" Fixed="true" Class="px-4 pt-4">
<div class="d-flex flex-column">
<Content>
<p>Are you sure you want to delete this team(s)?</p>
</Content>
<Footer>
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="@DeleteSelectedTeams">Confirm</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="@CloseToggleConf">Close</MudButton>
</Footer>
</div>
</MudPopover>
<MudDataGrid
T="Team"
RowsPerPage="4"
ServerData="GetTeamsFromServer"
@ref="Grid"
MultiSelection="true"
@bind-SelectedItems="SelectedTeams"
ReadOnly="false"
EditMode="DataGridEditMode.Form"
CommittedItemChanges="OnTeamUpdated">
<Columns>
<SelectColumn T="Team"/>
<PropertyColumn
Property="x => x.Name"/>
<PropertyColumn
Property="x => x.MainColor"/>
<PropertyColumn
Property="x => x.SecondColor"/>
<PropertyColumn
Property="x => x.Picture"/>
<TemplateColumn>
<CellTemplate>
<MudIconButton Size="Size.Small" Icon="@Icons.Material.Outlined.Edit" OnClick="@context.Actions.StartEditingItemAsync"/>
</CellTemplate>
</TemplateColumn>
</Columns>
<PagerContent>
<MudDataGridPager T="Team" PageSizeOptions="new[] { 1, 2, 4, 10, 25, 50, 100 }"/>
</PagerContent>
</MudDataGrid>

@ -0,0 +1,109 @@
using System.ComponentModel.DataAnnotations;
using System.Text;
using AdminPanel.Models;
using AdminPanel.Services;
using Blazorise.Extensions;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace AdminPanel.Pages;
public partial class TeamListPanel
{
[Inject] private ISnackbar Snackbar { get; init; }
[Inject] private ITeamService TeamService { get; init; }
private bool _isOpenAdd;
private bool _isOpenConf;
private bool _success;
private string[] _errors = { };
[StringLength(1,ErrorMessage = "Nombre de caractères minimal : 1 ")]
private string? FormName { get; set; }
[RegularExpression(@"(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?", ErrorMessage = "L'image doit être le lien d'une image sur le web")]
private string? FormPicture { get; set; }
[RegularExpression(@"(/#[0-9a-f]{6})",ErrorMessage = "La couleur principale doit être un code hexadécimal")]
private string? FormMainColor { get; set; }
[RegularExpression(@"(/#[0-9a-f]{6})",ErrorMessage = "La couleur secondaire doit être un code hexadécimal")]
private string? FormSecondaryColor { get; set; }
private MudDataGrid<Team>? Grid { get; set; }
private HashSet<Team> SelectedTeams { get; set; } = new();
private void UnBindForm()
{
FormName = null;
FormPicture = null;
FormMainColor = null;
FormSecondaryColor = null;
}
private async Task<GridData<Team>> GetTeamsFromServer(GridState<Team> state)
{
var (count, teams) = await TeamService.ListTeam((uint)(state.Page * state.PageSize), (uint)state.PageSize);
return new GridData<Team>
{
TotalItems = (int)count,
Items = teams
};
}
private void ToggleOpen()
{
_isOpenAdd = !(_isOpenAdd);
}
private void ToggleOpenConfirmation()
{
_isOpenConf = !(_isOpenConf);
}
private async Task AddTeam()
{
try
{
await TeamService.AddTeam(FormName!, FormPicture!, FormMainColor!, FormSecondaryColor!);
}
catch (ServiceException err)
{
DisplayUtils.ShowErrors(err,Snackbar);
}
}
private async void AddTeamConfirmed()
{
await AddTeam();
ToggleOpen();
await Grid!.ReloadServerData();
UnBindForm();
}
private async void DeleteSelectedTeams()
{
if (!SelectedTeams.IsNullOrEmpty())
{
var ids = SelectedTeams.ToList().ConvertAll(team => team.Id);
await TeamService.DeleteTeams(ids);
await Grid!.ReloadServerData();
}
CloseToggleConf();
}
private void CloseToggleConf()
{
_isOpenConf = false;
}
private async void OnTeamUpdated(Team t)
{
try
{
await TeamService.UpdateTeam(t);
}
catch (ServiceException err)
{
DisplayUtils.ShowErrors(err,Snackbar);
}
}
}

@ -52,7 +52,7 @@ namespace AdminPanel.Pages
}
catch (ServiceException err)
{
ShowErrors(err);
DisplayUtils.ShowErrors(err,Snackbar);
}
}
@ -70,25 +70,10 @@ namespace AdminPanel.Pages
}
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());
DisplayUtils.ShowErrors(err,Snackbar);
}
}
private async void RemoveSelection(MouseEventArgs e)
{
@ -101,7 +86,7 @@ namespace AdminPanel.Pages
}
catch (ServiceException err)
{
ShowErrors(err);
DisplayUtils.ShowErrors(err,Snackbar);
}
}

@ -1,5 +1,4 @@
using AdminPanel;
using AdminPanel.Pages;
using AdminPanel.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@ -8,7 +7,6 @@ using MudBlazor.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services
.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
@ -16,13 +14,15 @@ builder.Services
var client = new HttpClient
{
BaseAddress = new Uri("http://localhost:8080")
BaseAddress = new Uri("http://localhost:5254")
};
builder.Services.AddScoped<IUsersService>(sp => new HttpUsersService(client));
//builder.Services.AddScoped<IUsersService, UsersServiceStub>();
builder.Services.AddScoped<ITeamService>(sp => new HttpTeamService(client));
builder.Services.AddMudServices();
await builder.Build().RunAsync();
await builder.Build().RunAsync();

@ -0,0 +1,18 @@
# instructions
Avant de démarrer cette application, il faut tout d'abord démarrer le serveur :
```bash
git clone https://codefirst.iut.uca.fr/git/IQBall/Appliction-Web.git
cd Appliction-Web
dotnet run --project API
```
puis, dans un second terminal, démarrer ce projet :
```
git clone https://codefirst.iut.uca.fr/git/IQBall/Server-Panel
cd Server-Panel
dotnet run
```

@ -0,0 +1,27 @@
using System.Net;
using System.Net.Http.Json;
namespace AdminPanel.Services;
public class ErrorsUtils
{
private record ServerErrorMessageDto(string? Field, string? Error, string Message);
public static async Task EnsureResponseIsOk(HttpResponseMessage response)
{
if (response.StatusCode == HttpStatusCode.OK)
{
return;
}
var content = await response.Content.ReadFromJsonAsync<ServerErrorMessageDto[]>();
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);
}
}

@ -0,0 +1,57 @@
using System.Net.Http.Json;
using AdminPanel.Models;
namespace AdminPanel.Services;
public class HttpTeamService : ITeamService
{
private readonly HttpClient _client;
public HttpTeamService(HttpClient client)
{
this._client = client;
}
private record CountTeamsResponse(uint Value);
public async Task<(uint, List<Team>)> ListTeam(uint from, uint count)
{
var httpResponse = await _client.GetAsync($"/admin/teams?start={from}&n={count}");
httpResponse.EnsureSuccessStatusCode();
var teams = (await httpResponse.Content.ReadFromJsonAsync<List<Team>>())!;
httpResponse =
await _client.GetAsync($"/admin/teams/count");
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
var countResponse = await httpResponse.Content.ReadFromJsonAsync<CountTeamsResponse>();
return (countResponse!.Value, teams);
}
private record AddTeamRequest(string Name, string Picture, string FirstColor, string SecondColor);
public async Task AddTeam(string name, string picture, string mainColor, string secondaryColor)
{
var httpResponse = await _client.PostAsJsonAsync($"/admin/teams",
new AddTeamRequest(name, picture, mainColor, secondaryColor));
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
}
private record DeleteTeamsRequest(List<uint> Teams);
public async Task DeleteTeams(List<uint> teams)
{
var httpResponse = await _client.PostAsJsonAsync($"/admin/teams/remove-all", new DeleteTeamsRequest(teams));
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
}
private record UpdateTeamRequest(uint Id, string Name, string Picture, string MainColor, string SecondaryColor);
public async Task UpdateTeam(Team team)
{
var httpResponse = await _client.PutAsJsonAsync($"/admin/teams/{team.Id}",
new UpdateTeamRequest(team.Id, team.Name, team.Picture, team.MainColor, team.SecondColor));
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
}
}

@ -14,39 +14,27 @@ public class HttpUsersService : IUsersService
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<ServerErrorMessageDto[]>();
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<User> Users, uint TotalCount);
private record CountRequestResult(uint Value);
public async Task<(uint, List<User>)> 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);
await Client.GetAsync($"/admin/users?start={from}&n={len}&search={searchString ?? ""}");
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
var response = await httpResponse.Content.ReadFromJsonAsync<ListUsersResponseDto>()
?? throw new Exception("Received non-parseable json from server");
var users = await httpResponse.Content.ReadFromJsonAsync<List<User>>()
?? throw new Exception("Received non-parseable json from server");
var search = searchString != null ? $"&search={searchString}" : "";
httpResponse =
await Client.GetAsync($"/admin/users/count{search}");
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
var count = await httpResponse.Content.ReadFromJsonAsync<CountRequestResult>();
return (response.TotalCount, response.Users);
return (count!.Value, users);
}
private record AddUserRequestDto(string Username, string Email, string Password, bool IsAdmin);
@ -55,10 +43,10 @@ public class HttpUsersService : IUsersService
public async Task<User> AddUser(string username, string email, string password, bool isAdmin)
{
var httpResponse = await Client.PostAsJsonAsync("/api/admin/user/add",
var httpResponse = await Client.PostAsJsonAsync("/admin/users",
new AddUserRequestDto(username, email, password, isAdmin));
await EnsureResponseIsOk(httpResponse);
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
var response = await httpResponse.Content.ReadFromJsonAsync<AddUserResponseDto>()
?? throw new Exception("Received non-parseable json from server");
@ -77,17 +65,17 @@ public class HttpUsersService : IUsersService
public async Task RemoveUsers(IEnumerable<uint> userIds)
{
var httpResponse =
await Client.PostAsJsonAsync("/api/admin/user/remove-all", new RemoveUsersRequestDto(userIds.ToArray()));
await EnsureResponseIsOk(httpResponse);
await Client.PostAsJsonAsync("/admin/users/remove-all", new RemoveUsersRequestDto(userIds.ToArray()));
await ErrorsUtils.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",
var httpResponse = await Client.PutAsJsonAsync($"/admin/users/{user.Id}",
new UpdateUserRequestDto(user.Email, user.Name, user.IsAdmin));
await EnsureResponseIsOk(httpResponse);
await ErrorsUtils.EnsureResponseIsOk(httpResponse);
}
}

@ -0,0 +1,12 @@
using AdminPanel.Models;
namespace AdminPanel.Services;
public interface ITeamService
{
public Task<(uint, List<Team>)> ListTeam(uint from, uint count);
public Task AddTeam(string name, string picture, string mainColor, string secondaryColor);
public Task DeleteTeams(List<uint> teams);
public Task UpdateTeam(Team team);
}

@ -1,6 +1,9 @@
using System.Net;
using System.Net.Http.Json;
namespace AdminPanel.Services;
class ServiceException : Exception
public class ServiceException : Exception
{
public Dictionary<string, List<string>> ArgumentMessages { get; init; }
@ -15,4 +18,6 @@ class ServiceException : Exception
{
ArgumentMessages = arguments;
}
}

@ -1,9 +1,8 @@
@inherits LayoutComponentBase
@using MudBlazor
<MudThemeProvider/>
<MudThemeProvider IsDarkMode="true"/>
<MudDialogProvider/>
<MudSnackbarProvider/>
<div class="page">
<div class="sidebar">
<NavMenu />

@ -9,7 +9,8 @@ main {
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
background-image: linear-gradient(180deg, rgb(0, 0, 0) 0%, rgb(80, 80, 80) 70%);
/*background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);*/
}
.top-row {

@ -1,6 +1,6 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">Application-Administration</a>
<a class="navbar-brand" href="">IQBall-Administration</a>
<button title="Navigation menu" class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
@ -15,8 +15,13 @@
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="/users">
<span class="oi oi-plus" aria-hidden="true"></span> Users
<NavLink class="nav-link" href="/users">
<span class="oi oi-plus" aria-hidden="true"></span> Users
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="/teams">
<span class="oi oi-plus" aria-hidden="true"></span> Teams
</NavLink>
</div>
</nav>

@ -16,6 +16,7 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
</head>
<body>

Loading…
Cancel
Save