mirror of
https://github.com/LBPUnion/ProjectLighthouse.git
synced 2025-07-23 05:31:29 +00:00
Improve moderation case page and case partial (#900)
* Add pagination to moderation cases list and tweak case dismissal task * Clean up case partial and add extended case status indicators * Redirect back to cases list after dismissing a case * Fix typo on cases list queue counter * Fix dismissal queue counter * Convert dismiss button check into pattern * Turn down case dismissal task repeat interval to every 1 hour * Use page 0 for case searching * Implement pagination on the admin users list <3 * Fix pagination button padding and update colors to match existing role colors * Fix typo in admin search placeholder * Make cases searchable by user/slot ID instead of reason Due to the current state of the moderation case entity, I can't directly query against the affected user name, so I've added the ability to search for the affected user/slot ID instead of reason. * Actually apply the desired changes instead of just fixing the counts * Grammatical nitpick in the search box placeholder
This commit is contained in:
parent
1ddedae1fb
commit
3a2cdc9afe
9 changed files with 147 additions and 61 deletions
|
@ -108,6 +108,6 @@ public class AdminUserController : ControllerBase
|
|||
return this.Redirect($"/moderation/newCase?type={(int)CaseType.UserBan}&affectedId={id}");
|
||||
}
|
||||
|
||||
return this.Redirect("/admin/users");
|
||||
return this.Redirect("/admin/users/0");
|
||||
}
|
||||
}
|
|
@ -34,6 +34,6 @@ public class ModerationCaseController : ControllerBase
|
|||
|
||||
await this.database.SaveChangesAsync();
|
||||
|
||||
return this.Ok();
|
||||
return this.Redirect($"/moderation/cases/0");
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ public class AdminPanelPage : BaseLayout
|
|||
if (user == null) return this.Redirect("~/login");
|
||||
if (!user.IsAdmin) return this.NotFound();
|
||||
|
||||
this.Statistics.Add(new AdminPanelStatistic("Users", await StatisticsHelper.UserCount(this.Database), "/admin/users"));
|
||||
this.Statistics.Add(new AdminPanelStatistic("Users", await StatisticsHelper.UserCount(this.Database), "/admin/users/0"));
|
||||
this.Statistics.Add(new AdminPanelStatistic("Slots", await StatisticsHelper.SlotCount(this.Database, new SlotQueryBuilder())));
|
||||
this.Statistics.Add(new AdminPanelStatistic("Photos", await StatisticsHelper.PhotoCount(this.Database)));
|
||||
this.Statistics.Add(new AdminPanelStatistic("API Keys", await StatisticsHelper.ApiKeyCount(this.Database), "/admin/keys"));
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@page "/admin/users"
|
||||
@page "/admin/users/{pageNumber:int}"
|
||||
@using LBPUnion.ProjectLighthouse.Types.Entities.Profile
|
||||
@using LBPUnion.ProjectLighthouse.Types.Users
|
||||
@model LBPUnion.ProjectLighthouse.Servers.Website.Pages.Admin.AdminPanelUsersPage
|
||||
|
@ -11,6 +11,15 @@
|
|||
<p>There are currently @Model.UserCount users registered to your instance.</p>
|
||||
<p><b>Note:</b> Users are ordered by their permissions, then by most-recent-first.</p>
|
||||
|
||||
<form action="/admin/users/0">
|
||||
<div class="ui icon input">
|
||||
<input type="text" autocomplete="off" name="name" placeholder="Search users..." value="@Model.SearchValue">
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="ui divider"></div>
|
||||
|
||||
<div class="ui grid">
|
||||
@foreach (UserEntity user in Model.Users)
|
||||
{
|
||||
|
@ -19,22 +28,21 @@
|
|||
|
||||
switch (user.PermissionLevel)
|
||||
{
|
||||
// jank but works
|
||||
case PermissionLevel.Banned:
|
||||
{
|
||||
color = "red";
|
||||
color = "grey";
|
||||
subtitle = $"Banned user! Reason: {user.BannedReason}";
|
||||
break;
|
||||
}
|
||||
case PermissionLevel.Moderator:
|
||||
{
|
||||
color = "green";
|
||||
color = "orange";
|
||||
subtitle = "Moderator";
|
||||
break;
|
||||
}
|
||||
case PermissionLevel.Administrator:
|
||||
{
|
||||
color = "yellow";
|
||||
color = "red";
|
||||
subtitle = "Admin";
|
||||
break;
|
||||
}
|
||||
|
@ -72,4 +80,16 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br />
|
||||
|
||||
@if (Model.PageNumber != 0)
|
||||
{
|
||||
<a href="/admin/users/@(Model.PageNumber - 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
|
||||
}
|
||||
@(Model.PageNumber + 1) / @(Model.PageAmount)
|
||||
@if (Model.PageNumber < Model.PageAmount - 1)
|
||||
{
|
||||
<a href="/admin/users/@(Model.PageNumber + 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
#nullable enable
|
||||
using LBPUnion.ProjectLighthouse.Configuration;
|
||||
using LBPUnion.ProjectLighthouse.Database;
|
||||
using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts;
|
||||
using LBPUnion.ProjectLighthouse.Types.Entities.Profile;
|
||||
|
@ -12,21 +13,39 @@ public class AdminPanelUsersPage : BaseLayout
|
|||
public int UserCount;
|
||||
|
||||
public List<UserEntity> Users = new();
|
||||
|
||||
public int PageAmount;
|
||||
public int PageNumber;
|
||||
public string SearchValue = "";
|
||||
|
||||
public AdminPanelUsersPage(DatabaseContext database) : base(database)
|
||||
{}
|
||||
|
||||
public async Task<IActionResult> OnGet()
|
||||
public async Task<IActionResult> OnGet([FromRoute] int pageNumber, [FromQuery] string? name)
|
||||
{
|
||||
UserEntity? user = this.Database.UserFromWebRequest(this.Request);
|
||||
if (user == null) return this.Redirect("~/login");
|
||||
if (!user.IsAdmin) return this.NotFound();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name)) name = "";
|
||||
|
||||
this.SearchValue = name.Replace(" ", string.Empty);
|
||||
|
||||
this.UserCount = await this.Database.Users.CountAsync(u => u.Username.Contains(this.SearchValue));
|
||||
|
||||
this.PageNumber = pageNumber;
|
||||
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.UserCount / ServerStatics.PageSize));
|
||||
|
||||
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount)
|
||||
return this.Redirect($"/admin/users/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
|
||||
|
||||
this.Users = await this.Database.Users
|
||||
.OrderByDescending(u => u.PermissionLevel)
|
||||
.ThenByDescending(u => u.UserId)
|
||||
.Where(u => u.Username.Contains(this.SearchValue))
|
||||
.Skip(pageNumber * ServerStatics.PageSize)
|
||||
.Take(ServerStatics.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
this.UserCount = this.Users.Count;
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
string timeZone = Model.GetTimeZone();
|
||||
}
|
||||
|
||||
<p>There are @Model.CaseCount total cases, @Model.DismissedCaseCount of which have been dismissed.</p>
|
||||
<p>There are @Model.CaseCount total cases, @Model.ExpiredCaseCount of which are queued for dismissal, and @Model.DismissedCaseCount of which have been dismissed.</p>
|
||||
|
||||
<form action="/moderation/cases/0">
|
||||
<div class="ui icon input">
|
||||
<input type="text" autocomplete="off" name="name" placeholder="Search cases..." value="@Model.SearchValue">
|
||||
<input type="text" autocomplete="off" name="name" placeholder="Search by affected ID..." value="@Model.SearchValue">
|
||||
<i class="search icon"></i>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -24,4 +24,14 @@
|
|||
@foreach (ModerationCaseEntity @case in Model.Cases)
|
||||
{
|
||||
@(await Html.PartialAsync("Partials/ModerationCasePartial", @case, ViewData.WithTime(timeZone)))
|
||||
}
|
||||
|
||||
@if (Model.PageNumber != 0)
|
||||
{
|
||||
<a href="/moderation/cases/@(Model.PageNumber - 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Previous Page</a>
|
||||
}
|
||||
@(Model.PageNumber + 1) / @(Model.PageAmount)
|
||||
@if (Model.PageNumber < Model.PageAmount - 1)
|
||||
{
|
||||
<a href="/moderation/cases/@(Model.PageNumber + 1)@(Model.SearchValue?.Length == 0 ? "" : "?name=" + Model.SearchValue)">Next Page</a>
|
||||
}
|
|
@ -11,10 +11,11 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Moderation;
|
|||
public class CasePage : BaseLayout
|
||||
{
|
||||
public CasePage(DatabaseContext database) : base(database)
|
||||
{}
|
||||
{ }
|
||||
|
||||
public List<ModerationCaseEntity> Cases = new();
|
||||
public int CaseCount;
|
||||
public int ExpiredCaseCount;
|
||||
public int DismissedCaseCount;
|
||||
|
||||
public int PageAmount;
|
||||
|
@ -31,18 +32,30 @@ public class CasePage : BaseLayout
|
|||
|
||||
this.SearchValue = name.Replace(" ", string.Empty);
|
||||
|
||||
this.Cases = await this.Database.Cases
|
||||
.Include(c => c.Creator)
|
||||
.Include(c => c.Dismisser)
|
||||
.OrderByDescending(c => c.CaseId)
|
||||
.ToListAsync();
|
||||
|
||||
this.CaseCount = await this.Database.Cases.CountAsync(c => c.Reason.Contains(this.SearchValue));
|
||||
this.DismissedCaseCount = await this.Database.Cases.CountAsync(c => c.Reason.Contains(this.SearchValue) && c.DismissedAt != null);
|
||||
this.CaseCount =
|
||||
await this.Database.Cases.CountAsync(c =>
|
||||
c.AffectedId.ToString().Contains(this.SearchValue));
|
||||
this.ExpiredCaseCount =
|
||||
await this.Database.Cases.CountAsync(c =>
|
||||
c.AffectedId.ToString().Contains(this.SearchValue) && c.DismissedAt == null && c.ExpiresAt < DateTime.UtcNow);
|
||||
this.DismissedCaseCount =
|
||||
await this.Database.Cases.CountAsync(c =>
|
||||
c.AffectedId.ToString().Contains(this.SearchValue)&& c.DismissedAt != null);
|
||||
|
||||
this.PageNumber = pageNumber;
|
||||
this.PageAmount = Math.Max(1, (int)Math.Ceiling((double)this.CaseCount / ServerStatics.PageSize));
|
||||
|
||||
if (this.PageNumber < 0 || this.PageNumber >= this.PageAmount)
|
||||
return this.Redirect($"/moderation/cases/{Math.Clamp(this.PageNumber, 0, this.PageAmount - 1)}");
|
||||
|
||||
this.Cases = await this.Database.Cases.Include(c => c.Creator)
|
||||
.Include(c => c.Dismisser)
|
||||
.Where(c => c.AffectedId.ToString().Contains(this.SearchValue))
|
||||
.OrderByDescending(c => c.CaseId)
|
||||
.Skip(pageNumber * ServerStatics.PageSize)
|
||||
.Take(ServerStatics.PageSize)
|
||||
.ToListAsync();
|
||||
|
||||
return this.Page();
|
||||
}
|
||||
}
|
|
@ -7,42 +7,19 @@
|
|||
@inject DatabaseContext Database
|
||||
|
||||
@{
|
||||
string color = "blue";
|
||||
|
||||
string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id;
|
||||
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
|
||||
|
||||
if (Model.Expired) color = "yellow";
|
||||
if (Model.Dismissed) color = "green";
|
||||
string color = "red";
|
||||
|
||||
if (Model.Expired)
|
||||
color = "yellow";
|
||||
if (Model.Dismissed)
|
||||
color = "green";
|
||||
}
|
||||
|
||||
<div class="ui @color segment">
|
||||
<h2>Case #@Model.CaseId: @Model.Type</h2>
|
||||
|
||||
@if (Model.Dismissed)
|
||||
{
|
||||
Debug.Assert(Model.DismissedAt != null);
|
||||
|
||||
@if (Model.Dismisser != null)
|
||||
{
|
||||
<h3 class="ui @color header">
|
||||
This case was dismissed by <a href="/user/@Model.Dismisser.UserId">@Model.DismisserUsername</a> on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
|
||||
</h3>
|
||||
}
|
||||
else
|
||||
{
|
||||
<h3 class="ui @color header">
|
||||
This case was dismissed by @Model.DismisserUsername on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
|
||||
</h3>
|
||||
}
|
||||
|
||||
}
|
||||
else if (Model.Expired)
|
||||
{
|
||||
<h3 class="ui @color header">
|
||||
This case expired on @TimeZoneInfo.ConvertTime(Model.ExpiresAt!.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
|
||||
</h3>
|
||||
}
|
||||
|
||||
@if (Model.Creator != null && Model.Creator.Username.Length != 0)
|
||||
{
|
||||
|
@ -66,14 +43,15 @@
|
|||
}
|
||||
</span><br>
|
||||
}
|
||||
|
||||
|
||||
|
||||
@if (Model.Type.AffectsLevel())
|
||||
{
|
||||
SlotEntity? slot = await Model.GetSlotAsync(Database);
|
||||
if (slot != null)
|
||||
{
|
||||
<p><strong>Affected level:</strong> <a href="/slot/@slot.SlotId">@slot.Name</a></p>
|
||||
<p>
|
||||
<strong>Affected level:</strong> <a href="/slot/@slot.SlotId">@slot.Name (@slot.SlotId)</a>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
else if (Model.Type.AffectsUser())
|
||||
|
@ -81,10 +59,55 @@
|
|||
UserEntity? user = await Model.GetUserAsync(Database);
|
||||
if (user != null)
|
||||
{
|
||||
<p><strong>Affected user:</strong> <a href="/user/@user.UserId">@user.Username</a></p>
|
||||
<p>
|
||||
<strong>Affected user:</strong> <a href="/user/@user.UserId">@user.Username (@user.UserId)</a>
|
||||
</p>
|
||||
}
|
||||
}
|
||||
|
||||
<h3>Case Status</h3>
|
||||
@if (Model.Dismissed)
|
||||
{
|
||||
Debug.Assert(Model.DismissedAt != null);
|
||||
|
||||
@if (Model.Dismisser != null)
|
||||
{
|
||||
<div>
|
||||
<i class="ui green icon check"></i>
|
||||
<span class="ui green text">
|
||||
This case was dismissed by <a href="/user/@Model.Dismisser.UserId">@Model.DismisserUsername</a> on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
<i class="ui green icon check"></i>
|
||||
<span class="ui green text">
|
||||
This case was dismissed by @Model.DismisserUsername on @TimeZoneInfo.ConvertTime(Model.DismissedAt.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else if (Model.Expired)
|
||||
{
|
||||
<div>
|
||||
<i class="ui orange icon clock"></i>
|
||||
<span class="ui orange text">
|
||||
This case expired on @TimeZoneInfo.ConvertTime(Model.ExpiresAt!.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt") and has been queued for dismissal.
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>
|
||||
<i class="ui red icon times"></i>
|
||||
<span class="ui red text">
|
||||
This case is currently active and will expire on @TimeZoneInfo.ConvertTime(Model.ExpiresAt!.Value, timeZoneInfo).ToString("M/d/yyyy @ h:mm tt").
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<h3>Reason</h3>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.Reason))
|
||||
{
|
||||
|
@ -94,7 +117,7 @@
|
|||
{
|
||||
<pre><b>No reason was provided.</b></pre>
|
||||
}
|
||||
|
||||
|
||||
<h3>Moderator Notes</h3>
|
||||
@if (!string.IsNullOrWhiteSpace(Model.ModeratorNotes))
|
||||
{
|
||||
|
@ -104,8 +127,8 @@
|
|||
{
|
||||
<pre><b>No notes were provided.</b></pre>
|
||||
}
|
||||
|
||||
@if (!Model.Dismissed)
|
||||
|
||||
@if (Model is { Dismissed: false, Expired: false, })
|
||||
{
|
||||
<a class="ui green small button" href="/moderation/case/@Model.CaseId/dismiss">
|
||||
<i class="checkmark icon"></i>
|
||||
|
|
|
@ -14,7 +14,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.RepeatingTasks;
|
|||
public class DismissExpiredCasesTask : IRepeatingTask
|
||||
{
|
||||
public string Name => "Dismiss Expired Cases";
|
||||
public TimeSpan RepeatInterval => TimeSpan.FromHours(4);
|
||||
public TimeSpan RepeatInterval => TimeSpan.FromHours(1);
|
||||
public DateTime LastRan { get; set; }
|
||||
|
||||
public async Task Run(DatabaseContext database)
|
||||
|
@ -31,7 +31,8 @@ public class DismissExpiredCasesTask : IRepeatingTask
|
|||
|
||||
foreach (ModerationCaseEntity @case in expiredCases)
|
||||
{
|
||||
@case.DismissedAt = DateTime.Now;
|
||||
@case.DismissedAt = DateTime.UtcNow;
|
||||
@case.DismisserUsername = "maintenance task";
|
||||
Logger.Info($"Dismissed expired case {@case.CaseId}", LogArea.Maintenance);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue