a fork of iceshrimp.net but a tweaked frontend to my personal liking. waow
fediverse social-media social iceshrimp fedi
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[backend/razor] Port Queue Dashboard to .razor

authored by

pancakes and committed by
Laura Hausmann
54cee381 66bb0eaa

+266 -224
+24
Iceshrimp.Backend/Components/Queue/QueueHead.razor
··· 1 + @using Iceshrimp.Backend.Components.Helpers 2 + @using Microsoft.AspNetCore.Components.Web 3 + 4 + <PageTitle>@Title - Queue - @(InstanceName ?? "Iceshrimp.NET")</PageTitle> 5 + 6 + <HeadContent> 7 + <VersionedLink rel="stylesheet" href="/css/queue.css"/> 8 + <VersionedLink rel="stylesheet" href="/_content/Iceshrimp.Assets.PhosphorIcons/css/ph-regular.css"/> 9 + <VersionedScript src="/js/queue.js"/> 10 + @if (QueueIndex) 11 + { 12 + <VersionedScript src="/js/queue-index.js"/> 13 + } 14 + </HeadContent> 15 + 16 + <QueueNav/> 17 + 18 + @code { 19 + [Parameter, EditorRequired] public required string Title { get; set; } 20 + [Parameter] public bool QueueIndex { get; set; } 21 + 22 + [CascadingParameter(Name = "InstanceName")] 23 + public required string? InstanceName { get; set; } 24 + }
+36
Iceshrimp.Backend/Components/Queue/QueueNav.razor
··· 1 + @using Iceshrimp.Assets.PhosphorIcons 2 + @using Iceshrimp.Backend.Components.Generic 3 + @using Iceshrimp.Backend.Core.Services 4 + @inject QueueService Queue; 5 + 6 + <NavBar Brand="_brand" Links="_links" Right="_right" MaxItemsLg="7" MaxItemsMd="3"/> 7 + 8 + @code { 9 + private NavBar.NavLink _brand = new("/queue", "Queue Dashboard"); 10 + 11 + private Dictionary<string, IconName> _icons = new Dictionary<string, IconName>() 12 + { 13 + {"inbox", Icons.Envelope}, 14 + {"pre-deliver", Icons.Package}, 15 + {"deliver", Icons.Truck}, 16 + {"background-task", Icons.ListChecks}, 17 + {"backfill", Icons.Archive}, 18 + {"backfill-user", Icons.FolderUser} 19 + }; 20 + 21 + private List<NavBar.NavLink> _links = 22 + [ 23 + new("/queue", "Overview", Icons.ChartLine) 24 + ]; 25 + 26 + private List<NavBar.NavLink> _right = [ 27 + new("/admin", "Admin Dashboard", IconRight: Icons.ArrowSquareOut, NewTab: true), 28 + new("/mod", "Moderation Dashboard", IconRight: Icons.ArrowSquareOut, NewTab: true) 29 + ]; 30 + 31 + protected override void OnParametersSet() 32 + { 33 + var links = Queue.QueueNames.Select(p => new NavBar.NavLink($"/queue/{p}", p, _icons.GetValueOrDefault(p))); 34 + _links = _links.Concat(links).ToList(); 35 + } 36 + }
+73 -81
Iceshrimp.Backend/Pages/Queue.cshtml Iceshrimp.Backend/Pages/Queue/Queue.razor
··· 1 - @page "/queue/{queue?}/{pagination:int?}/{status?}" 2 - @inject QueueService QueueSvc 1 + @page "/queue/{Name?}/{Pagination:int?}/{Status?}" 2 + @using Iceshrimp.Backend.Components.Queue 3 3 @using Iceshrimp.Backend.Core.Database.Tables 4 4 @using Iceshrimp.Backend.Core.Extensions 5 - @using Iceshrimp.Backend.Core.Services 6 - @model QueueModel 7 - 8 - @{ 9 - ViewData["title"] = $"Queue dashboard - {Model.InstanceName}"; 10 - } 5 + @inherits AdminComponentBase 11 6 12 - @section head { 13 - <link rel="stylesheet" href="~/css/queue.css"/> 14 - } 7 + <QueueHead Title="@(Name ?? "Overview")" QueueIndex="@(Name == null)"/> 15 8 16 - @section scripts { 17 - <script src="~/js/queue.js"></script> 18 - @if (Model.Queue == null) 19 - { 20 - <script src="~/js/queue-index.js"></script> 21 - } 22 - } 23 - 24 - <h1>Queue Dashboard</h1> 25 - <button class="button" role="link" data-target="/queue" onclick="navigate(event)">overview</button> 26 - @foreach (var queue in QueueSvc.QueueNames) 9 + @if (Name == null) 27 10 { 28 - //asd 29 - <button class="button" role="link" data-target="/queue/@queue" onclick="navigate(event)">@queue</button> 30 - } 31 - <br/> 32 - 33 - @if (Model.Queue == null) 34 - { 35 - <p>Please pick a queue.</p> 36 - 37 11 <form onsubmit="return lookupJob(event)"> 38 12 <input type="text" id="lookup" placeholder="Lookup job by id" minlength="36" maxlength="36" class="inline"/> 39 13 <button class="button" type="submit">Submit</button> 40 14 </form> 41 15 42 16 <h3>Queue status &mdash; <span id="update-status" class="status-delayed">Connecting...</span></h3> 17 + 43 18 <table class="auto-table" id="queue-status"> 44 19 <thead> 45 20 <th>Name</th> ··· 50 25 } 51 26 </thead> 52 27 <tbody> 53 - @foreach (var queue in Model.QueueStatuses ?? throw new Exception("Model.QueueStatuses must not be null here")) 28 + @foreach (var queue in QueueStatuses ?? throw new Exception("QueueStatuses must not be null here")) 54 29 { 55 - var isScheduled = QueueModel.ScheduledQueues.Contains(queue.Name); 30 + var isScheduled = ScheduledQueues.Contains(queue.Name); 56 31 var scheduled = isScheduled ? queue.JobCounts[Job.JobStatus.Delayed].ToString() : "-"; 57 32 var delayed = !isScheduled ? queue.JobCounts[Job.JobStatus.Delayed].ToString() : "-"; 58 33 ··· 104 79 <th>Actions</th> 105 80 </thead> 106 81 <tbody> 107 - @foreach (var job in Model.Jobs) 82 + @foreach (var job in Jobs) 108 83 { 109 - await RenderJobAsync(job, true); 84 + @RenderJob(job, true) 85 + ; 110 86 } 111 87 </tbody> 112 88 </table> ··· 118 94 <th>Count</th> 119 95 </thead> 120 96 <tbody> 121 - @foreach (var instance in Model.TopDelayed) 97 + @foreach (var instance in TopDelayed) 122 98 { 123 99 <tr> 124 100 <td>@instance.Host</td> ··· 127 103 } 128 104 </tbody> 129 105 </table> 130 - @if (Model.TopDelayed is []) 106 + @if (TopDelayed is []) 131 107 { 132 108 <i>No delayed jobs found.</i> 133 109 } 134 110 135 - var last = Model.Jobs.FirstOrDefault(); 111 + var last = Jobs.FirstOrDefault(); 136 112 var lastUpdated = last != null ? new DateTimeOffset(last.LastUpdatedAt).ToUnixTimeMilliseconds() : 0; 137 113 138 114 <div class="display-none" id="last-updated">@lastUpdated</div> 139 115 } 140 - 141 116 else 142 117 { 143 - var delayedStr = QueueModel.ScheduledQueues.Contains(Model.Queue) ? "scheduled" : "delayed"; 144 - if (Model.Filter == null) 118 + var delayedStr = ScheduledQueues.Contains(Name) ? "scheduled" : "delayed"; 119 + if (Filter == null) 145 120 { 146 - <p>Listing @Model.TotalCount <b>@Model.Queue</b> jobs, out of which <span class="status-running">@Model.RunningCount</span> are <span class="status-running">running</span>, <span class="status-queued">@Model.QueuedCount</span> are <span class="status-queued">queued</span> and <span class="status-@delayedStr">@Model.DelayedCount</span> are <span class="status-@delayedStr">@delayedStr</span>.</p> 121 + <p>Listing @TotalCount <b>@Name</b> jobs, out of which <span class="status-running">@RunningCount</span> are 122 + <span class="status-running">running</span>, <span class="status-queued">@QueuedCount</span> are <span 123 + class="status-queued">queued</span> and <span class="status-@delayedStr">@DelayedCount</span> are <span 124 + class="status-@delayedStr">@delayedStr</span>.</p> 147 125 } 148 126 else 149 127 { 150 - var filterStr = Model.Filter.Value == Job.JobStatus.Delayed && QueueModel.ScheduledQueues.Contains(Model.Queue) 128 + var filterStr = Filter.Value == Job.JobStatus.Delayed && ScheduledQueues.Contains(Name) 151 129 ? "scheduled" 152 - : Model.Filter.Value.ToString().ToLowerInvariant(); 130 + : Filter.Value.ToString().ToLowerInvariant(); 153 131 154 132 <p> 155 - Listing @Model.TotalCount <span class="status-@filterStr">@filterStr</span> <b>@Model.Queue</b> jobs. 156 - @if (Model.Filter is Job.JobStatus.Failed) 133 + Listing @TotalCount <span class="status-@filterStr">@filterStr</span> <b>@Name</b> jobs. 134 + @if (Filter is Job.JobStatus.Failed) 157 135 { 158 - <span>Batch retry: <a class="fake-link" onclick="retryAllFailed('@Model.Queue')">all failed</a> / <a class="fake-link" onclick="retryAllOnPage('@Model.Queue')">all on this page</a></span> 136 + <span>Batch retry: <a class="fake-link" onclick="retryAllFailed('@Name')">all failed</a> / <a 137 + class="fake-link" onclick="retryAllOnPage('@Name')">all on this page</a></span> 159 138 } 160 139 </p> 161 140 } ··· 167 146 <th>Actions</th> 168 147 </thead> 169 148 <tbody> 170 - @foreach (var job in Model.Jobs) 149 + @foreach (var job in Jobs) 171 150 { 172 - await RenderJobAsync(job); 151 + @RenderJob(job) 152 + ; 173 153 } 174 154 </tbody> 175 155 </table> 176 156 177 157 <div class="flex"> 178 - @if (Model.PrevPage != null) 158 + @if (PrevPage != null) 179 159 { 180 - if (Model.Filter.HasValue) 160 + if (Filter.HasValue) 181 161 { 182 - <button class="button" role="link" data-target="/queue/@Model.Queue/@Model.PrevPage/@Model.Filter.Value.ToString().ToLowerInvariant()" onclick="navigate">❮ Previous page</button> 162 + <button class="button" role="link" 163 + data-target="/queue/@Name/@PrevPage/@Filter.Value.ToString().ToLowerInvariant()" 164 + onclick="navigate">❮ Previous page 165 + </button> 183 166 } 184 167 else 185 168 { 186 - <button class="button" role="link" data-target="/queue/@Model.Queue/@Model.PrevPage" onclick="navigate(event)">❮ Previous page</button> 169 + <button class="button" role="link" data-target="/queue/@Name/@PrevPage" onclick="navigate(event)">❮ 170 + Previous page 171 + </button> 187 172 } 188 173 } 189 174 else ··· 191 176 <button class="button" disabled>❮ Previous page</button> 192 177 } 193 178 194 - @if (Model.NextPage != null) 179 + @if (NextPage != null) 195 180 { 196 - if (Model.Filter.HasValue) 181 + if (Filter.HasValue) 197 182 { 198 - <button class="button" role="link" data-target="/queue/@Model.Queue/@Model.NextPage/@Model.Filter.Value.ToString().ToLowerInvariant()" onclick="navigate(event)">Next page ❯</button> 183 + <button class="button" role="link" 184 + data-target="/queue/@Name/@NextPage/@Filter.Value.ToString().ToLowerInvariant()" 185 + onclick="navigate(event)">Next page ❯ 186 + </button> 199 187 } 200 188 else 201 189 { 202 - <button class="button" role="link" data-target="/queue/@Model.Queue/@Model.NextPage" onclick="navigate(event)">Next page ❯</button> 190 + <button class="button" role="link" data-target="/queue/@Name/@NextPage" onclick="navigate(event)">Next 191 + page ❯ 192 + </button> 203 193 } 204 194 } 205 195 else ··· 207 197 <button class="button" disabled>Next page ❯</button> 208 198 } 209 199 210 - <select onchange="filter('@Model.Queue')" id="filter" class="inline-flex"> 211 - @if (Model.Filter == null) 200 + <select onchange="filter('@Name')" id="filter" class="inline-flex"> 201 + @if (Filter == null) 212 202 { 213 203 <option value="all" selected>All</option> 214 204 } ··· 218 208 } 219 209 @foreach (var status in Enum.GetValues<Job.JobStatus>()) 220 210 { 221 - var statusStr = status == Job.JobStatus.Delayed && QueueModel.ScheduledQueues.Contains(Model.Queue) 211 + var statusStr = status == Job.JobStatus.Delayed && ScheduledQueues.Contains(Name) 222 212 ? "Scheduled" 223 213 : status.ToString(); 224 214 225 - if (Model.Filter.Equals(status)) 215 + if (Filter.Equals(status)) 226 216 { 227 217 <option value="@status.ToString().ToLowerInvariant()" selected>@statusStr</option> 228 218 } ··· 234 224 </select> 235 225 236 226 <form onsubmit="return lookupJob(event)" class="inline-flex flex-grow"> 237 - <input type="text" id="lookup" placeholder="Lookup job by id" minlength="36" maxlength="36" class="flex-grow"/> 227 + <input type="text" id="lookup" placeholder="Lookup job by id" minlength="36" maxlength="36" 228 + class="flex-grow"/> 238 229 </form> 239 230 </div> 240 231 } 241 232 242 - @{ 243 - async Task RenderJobAsync(Job job, bool withQueue = false) 233 + @code { 234 + private RenderFragment RenderJob(Job job, bool withQueue = false) 244 235 { 245 236 var id = job.Id.ToStringLower(); 246 237 var additional = job.Status switch ··· 255 246 _ => throw new ArgumentOutOfRangeException() 256 247 }; 257 248 258 - var classes = Model.Last != null && new DateTimeOffset(job.LastUpdatedAt).ToUnixTimeMilliseconds() > Model.Last 249 + var classes = Last != null && new DateTimeOffset(job.LastUpdatedAt).ToUnixTimeMilliseconds() > Last 259 250 ? "new-item" 260 251 : ""; 261 252 262 253 var status = job is { Status: Job.JobStatus.Delayed, RetryCount: 0 } ? "Scheduled" : job.Status.ToString(); 263 254 264 - <tr class="@classes"> 265 - @if (withQueue) 266 - { 267 - <td class="uuid-abbrev">@id[..8]...@id[24..]</td> 268 - <td>@job.Queue</td> 269 - } 270 - else 271 - { 272 - <td class="uuid">@id</td> 273 - } 274 - <td class="status-@status.ToLowerInvariant()"> 275 - <b>@status</b> <small>@additional</small> 276 - <td> 277 - <a href="/queue/job/@id">View details</a> 278 - </td> 279 - </tr> 255 + return @<tr class="@classes"> 256 + @if (withQueue) 257 + { 258 + <td class="uuid-abbrev">@id[..8]...@id[24..]</td> 259 + <td>@job.Queue</td> 260 + } 261 + else 262 + { 263 + <td class="uuid">@id</td> 264 + } 265 + <td class="status-@status.ToLowerInvariant()"> 266 + <b>@status</b> <small>@additional</small> 267 + </td> 268 + <td> 269 + <a href="/queue/job/@id">View details</a> 270 + </td> 271 + </tr>; 280 272 } 281 273 }
-143
Iceshrimp.Backend/Pages/Queue.cshtml.cs
··· 1 - using System.Collections.Immutable; 2 - using Iceshrimp.Backend.Core.Database; 3 - using Iceshrimp.Backend.Core.Database.Tables; 4 - using Iceshrimp.Backend.Core.Middleware; 5 - using Iceshrimp.Backend.Core.Services; 6 - using Microsoft.AspNetCore.Mvc; 7 - using Microsoft.AspNetCore.Mvc.RazorPages; 8 - using Microsoft.EntityFrameworkCore; 9 - using static Iceshrimp.Backend.Core.Database.DatabaseContext; 10 - 11 - namespace Iceshrimp.Backend.Pages; 12 - 13 - public class QueueModel(DatabaseContext db, QueueService queueSvc, MetaService meta, CacheService cache) : PageModel 14 - { 15 - private NamedCache _cache = cache.GetNamedCache("admin:queue-dash"); 16 - 17 - public int? DelayedCount; 18 - public Job.JobStatus? Filter; 19 - public List<Job> Jobs = []; 20 - public int? NextPage; 21 - public int? PrevPage; 22 - public string? Queue; 23 - public int? QueuedCount; 24 - public int? RunningCount; 25 - public int? TotalCount; 26 - public List<QueueStatus>? QueueStatuses; 27 - public List<DelayedDeliverTarget> TopDelayed = []; 28 - public long? Last; 29 - public string InstanceName = "Iceshrimp.NET"; 30 - 31 - public static readonly ImmutableArray<string> ScheduledQueues = ["background-task", "backfill"]; 32 - 33 - public async Task<IActionResult> OnGet( 34 - [FromRoute] string? queue, [FromRoute(Name = "pagination")] int? page, [FromRoute] string? status 35 - ) 36 - { 37 - if (!Request.Cookies.TryGetValue("admin_session", out var cookie)) 38 - return Redirect("/login"); 39 - if (!await db.Sessions.AnyAsync(p => p.Token == cookie && p.Active && p.User.IsAdmin)) 40 - return Redirect("/login"); 41 - 42 - Request.HttpContext.HideFooter(); 43 - InstanceName = await meta.GetAsync(MetaEntity.InstanceName) ?? InstanceName; 44 - 45 - if (queue == null) 46 - { 47 - // Should be 15, but the table styles look more pleasing with even numbers 48 - Jobs = await db.Jobs.OrderByDescending(p => p.LastUpdatedAt).Take(16).ToListAsync(); 49 - if (Request.Query.TryGetValue("last", out var last) && long.TryParse(last, out var parsed)) 50 - Last = parsed; 51 - 52 - //TODO: write an expression generator for the job count calculation 53 - QueueStatuses = await db.Jobs 54 - .GroupBy(job => job.Queue) 55 - .OrderBy(p => p.Key) 56 - .Select(p => p.Key) 57 - .Select(queueName => new QueueStatus 58 - { 59 - Name = queueName, 60 - JobCounts = new Dictionary<Job.JobStatus, int> 61 - { 62 - { 63 - Job.JobStatus.Queued, db.Jobs.Count(job => 64 - job.Queue == queueName 65 - && job.Status 66 - == Job.JobStatus.Queued) 67 - }, 68 - { 69 - Job.JobStatus.Delayed, db.Jobs.Count(job => 70 - job.Queue == queueName && job.Status == Job.JobStatus.Delayed) 71 - }, 72 - { 73 - Job.JobStatus.Running, db.Jobs.Count(job => 74 - job.Queue == queueName && job.Status == Job.JobStatus.Running) 75 - }, 76 - { 77 - Job.JobStatus.Completed, db.Jobs.Count(job => 78 - job.Queue == queueName 79 - && job.Status == Job.JobStatus.Completed) 80 - }, 81 - { 82 - Job.JobStatus.Failed, db.Jobs.Count(job => 83 - job.Queue == queueName 84 - && job.Status 85 - == Job.JobStatus.Failed) 86 - } 87 - }.AsReadOnly() 88 - }) 89 - .ToListAsync(); 90 - 91 - TopDelayed = await _cache.FetchAsync("top-delayed", TimeSpan.FromSeconds(60), 92 - () => db.GetDelayedDeliverTargets().ToListAsync()); 93 - 94 - return Page(); 95 - } 96 - 97 - if (!queueSvc.QueueNames.Contains(queue)) 98 - throw GracefulException.BadRequest($"Unknown queue: {queue}"); 99 - 100 - Queue = queue; 101 - 102 - if (page is null or < 1) 103 - page = 1; 104 - 105 - var query = db.Jobs.Where(p => p.Queue == queue); 106 - if (status is { Length: > 0 }) 107 - { 108 - if (!Enum.TryParse<Job.JobStatus>(status, true, out var jobStatus)) 109 - throw GracefulException.BadRequest($"Unknown status: {status}"); 110 - query = query.Where(p => p.Status == jobStatus); 111 - Filter = jobStatus; 112 - } 113 - 114 - Jobs = await query.OrderByDescending(p => p.Id) 115 - .Skip((page.Value - 1) * 50) 116 - .Take(50) 117 - .ToListAsync(); 118 - 119 - if (Filter == null) 120 - { 121 - TotalCount = await db.Jobs.CountAsync(p => p.Queue == queue); 122 - QueuedCount = await db.Jobs.CountAsync(p => p.Queue == queue && p.Status == Job.JobStatus.Queued); 123 - RunningCount = await db.Jobs.CountAsync(p => p.Queue == queue && p.Status == Job.JobStatus.Running); 124 - DelayedCount = await db.Jobs.CountAsync(p => p.Queue == queue && p.Status == Job.JobStatus.Delayed); 125 - } 126 - else 127 - { 128 - TotalCount = await query.CountAsync(); 129 - } 130 - 131 - if (Jobs.Count >= 50) 132 - NextPage = page + 1; 133 - if (page is > 1) 134 - PrevPage = page - 1; 135 - return Page(); 136 - } 137 - 138 - public class QueueStatus 139 - { 140 - public required string Name { get; init; } 141 - public required IReadOnlyDictionary<Job.JobStatus, int> JobCounts { get; init; } 142 - } 143 - }
+133
Iceshrimp.Backend/Pages/Queue/Queue.razor.cs
··· 1 + using System.Collections.Immutable; 2 + using Iceshrimp.Backend.Components.Helpers; 3 + using Iceshrimp.Backend.Core.Database; 4 + using Iceshrimp.Backend.Core.Database.Tables; 5 + using Iceshrimp.Backend.Core.Middleware; 6 + using Iceshrimp.Backend.Core.Services; 7 + using Microsoft.AspNetCore.Components; 8 + using Microsoft.EntityFrameworkCore; 9 + using static Iceshrimp.Backend.Core.Database.DatabaseContext; 10 + 11 + namespace Iceshrimp.Backend.Pages.Queue; 12 + 13 + public partial class Queue(DatabaseContext db, QueueService queueSvc, CacheService cache) : AdminComponentBase 14 + { 15 + [Parameter] public string? Name { get; set; } 16 + [Parameter] public int? Pagination { get; set; } 17 + [Parameter] public string? Status { get; set; } 18 + 19 + private int? DelayedCount { get; set; } 20 + private int? QueuedCount { get; set; } 21 + private int? RunningCount { get; set; } 22 + private int? TotalCount { get; set; } 23 + private Job.JobStatus? Filter { get; set; } 24 + private List<Job> Jobs { get; set; } = []; 25 + private int? PrevPage { get; set; } 26 + private int? NextPage { get; set; } 27 + private List<QueueStatus>? QueueStatuses { get; set; } 28 + private List<DelayedDeliverTarget> TopDelayed { get; set; } = []; 29 + private long? Last { get; set; } 30 + 31 + private NamedCache _cache = cache.GetNamedCache("admin:queue-dash"); 32 + 33 + private static readonly ImmutableArray<string> ScheduledQueues = ["background-task", "backfill"]; 34 + 35 + private class QueueStatus 36 + { 37 + public required string Name { get; init; } 38 + public required IReadOnlyDictionary<Job.JobStatus, int> JobCounts { get; init; } 39 + } 40 + 41 + protected override async Task OnInitializedAsync() 42 + { 43 + if (Name == null) 44 + { 45 + // Should be 15, but the table styles look more pleasing with even numbers 46 + Jobs = await db.Jobs.OrderByDescending(p => p.LastUpdatedAt).Take(16).ToListAsync(); 47 + if (Context.Request.Query.TryGetValue("last", out var last) && long.TryParse(last, out var parsed)) 48 + Last = parsed; 49 + 50 + //TODO: write an expression generator for the job count calculation 51 + QueueStatuses = await db.Jobs 52 + .GroupBy(job => job.Queue) 53 + .OrderBy(p => p.Key) 54 + .Select(p => p.Key) 55 + .Select(queueName => new QueueStatus 56 + { 57 + Name = queueName, 58 + JobCounts = new Dictionary<Job.JobStatus, int> 59 + { 60 + { 61 + Job.JobStatus.Queued, db.Jobs.Count(job => 62 + job.Queue == queueName 63 + && job.Status 64 + == Job.JobStatus.Queued) 65 + }, 66 + { 67 + Job.JobStatus.Delayed, db.Jobs.Count(job => 68 + job.Queue == queueName && job.Status == Job.JobStatus.Delayed) 69 + }, 70 + { 71 + Job.JobStatus.Running, db.Jobs.Count(job => 72 + job.Queue == queueName && job.Status == Job.JobStatus.Running) 73 + }, 74 + { 75 + Job.JobStatus.Completed, db.Jobs.Count(job => 76 + job.Queue == queueName 77 + && job.Status == Job.JobStatus.Completed) 78 + }, 79 + { 80 + Job.JobStatus.Failed, db.Jobs.Count(job => 81 + job.Queue == queueName 82 + && job.Status 83 + == Job.JobStatus.Failed) 84 + } 85 + }.AsReadOnly() 86 + }) 87 + .ToListAsync(); 88 + 89 + TopDelayed = await _cache.FetchAsync("top-delayed", TimeSpan.FromSeconds(60), 90 + () => db.GetDelayedDeliverTargets().ToListAsync()); 91 + 92 + return; 93 + } 94 + 95 + if (!queueSvc.QueueNames.Contains(Name)) 96 + throw GracefulException.BadRequest($"Unknown queue: {Name}"); 97 + 98 + if (Pagination is null or < 1) 99 + Pagination = 1; 100 + 101 + var query = db.Jobs.Where(p => p.Queue == Name); 102 + if (Status is { Length: > 0 }) 103 + { 104 + if (!Enum.TryParse<Job.JobStatus>(Status, true, out var jobStatus)) 105 + throw GracefulException.BadRequest($"Unknown status: {Status}"); 106 + query = query.Where(p => p.Status == jobStatus); 107 + Filter = jobStatus; 108 + } 109 + 110 + Jobs = await query.OrderByDescending(p => p.Id) 111 + .Skip((Pagination.Value - 1) * 50) 112 + .Take(50) 113 + .ToListAsync(); 114 + 115 + if (Filter == null) 116 + { 117 + TotalCount = await db.Jobs.CountAsync(p => p.Queue == Name); 118 + QueuedCount = await db.Jobs.CountAsync(p => p.Queue == Name && p.Status == Job.JobStatus.Queued); 119 + RunningCount = await db.Jobs.CountAsync(p => p.Queue == Name && p.Status == Job.JobStatus.Running); 120 + DelayedCount = await db.Jobs.CountAsync(p => p.Queue == Name && p.Status == Job.JobStatus.Delayed); 121 + } 122 + else 123 + { 124 + TotalCount = await query.CountAsync(); 125 + } 126 + 127 + if (Jobs.Count >= 50) 128 + NextPage = Pagination + 1; 129 + if (Pagination is > 1) 130 + PrevPage = Pagination - 1; 131 + } 132 + } 133 +