281 lines
No EOL
10 KiB
Text
281 lines
No EOL
10 KiB
Text
@page "/queue/{queue?}/{pagination:int?}/{status?}"
|
||
@inject QueueService QueueSvc
|
||
@using Iceshrimp.Backend.Core.Database.Tables
|
||
@using Iceshrimp.Backend.Core.Extensions
|
||
@using Iceshrimp.Backend.Core.Services
|
||
@model QueueModel
|
||
|
||
@{
|
||
ViewData["title"] = $"Queue dashboard - {Model.InstanceName}";
|
||
}
|
||
|
||
@section head {
|
||
<link rel="stylesheet" href="~/css/queue.css"/>
|
||
}
|
||
|
||
@section scripts {
|
||
<script src="~/js/queue.js"></script>
|
||
@if (Model.Queue == null)
|
||
{
|
||
<script src="~/js/queue-index.js"></script>
|
||
}
|
||
}
|
||
|
||
<h1>Queue Dashboard</h1>
|
||
<button role="link" data-target="/queue" onclick="navigate(event)">overview</button>
|
||
@foreach (var queue in QueueSvc.QueueNames)
|
||
{
|
||
//asd
|
||
<button role="link" data-target="/queue/@queue" onclick="navigate(event)">@queue</button>
|
||
}
|
||
<br/>
|
||
|
||
@if (Model.Queue == null)
|
||
{
|
||
<p>Please pick a queue.</p>
|
||
|
||
<form onsubmit="return lookupJob(event)">
|
||
<input type="text" id="lookup" placeholder="Lookup job by id" minlength="36" maxlength="36" class="inline"/>
|
||
<button type="submit">Submit</button>
|
||
</form>
|
||
|
||
<h3>Queue status — <span id="update-status" class="status-delayed">Connecting...</span></h3>
|
||
<table class="auto-table" id="queue-status">
|
||
<thead>
|
||
<th>Name</th>
|
||
@foreach (var status in Enum.GetValues<Job.JobStatus>())
|
||
{
|
||
var name = Enum.GetName(status);
|
||
<th class="justify-right">@name</th>
|
||
}
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var queue in Model.QueueStatuses ?? throw new Exception("Model.QueueStatuses must not be null here"))
|
||
{
|
||
var isScheduled = QueueModel.ScheduledQueues.Contains(queue.Name);
|
||
var scheduled = isScheduled ? queue.JobCounts[Job.JobStatus.Delayed].ToString() : "-";
|
||
var delayed = !isScheduled ? queue.JobCounts[Job.JobStatus.Delayed].ToString() : "-";
|
||
|
||
<tr>
|
||
<td>
|
||
<a href="/queue/@queue.Name" class="color-fg">@queue.Name</a>
|
||
</td>
|
||
@foreach (var status in Enum.GetValues<Job.JobStatus>())
|
||
{
|
||
var name = Enum.GetName(status)!.ToLowerInvariant();
|
||
if (status == Job.JobStatus.Delayed)
|
||
{
|
||
if (!isScheduled)
|
||
{
|
||
<td class="justify-right">
|
||
<a href="/queue/@queue.Name/1/delayed" class="status-@name">@delayed</a>
|
||
</td>
|
||
}
|
||
else
|
||
{
|
||
<td class="justify-right status-@name">@delayed</td>
|
||
}
|
||
}
|
||
else if (status == Job.JobStatus.Queued && isScheduled)
|
||
{
|
||
<td class="justify-right">
|
||
(<a href="/queue/@queue.Name/1/delayed" class="status-scheduled">@scheduled</a>)
|
||
<a href="/queue/@queue.Name/1/queued" class="status-queued">@queue.JobCounts[status]</a>
|
||
</td>
|
||
}
|
||
else
|
||
{
|
||
<td class="justify-right">
|
||
<a href="/queue/@queue.Name/1/@name" class="status-@name">@queue.JobCounts[status]</a>
|
||
</td>
|
||
}
|
||
}
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>Recent jobs</h3>
|
||
<table class="auto-table" id="recent-jobs">
|
||
<thead>
|
||
<th class="width0">ID</th>
|
||
<th>Queue</th>
|
||
<th>Status</th>
|
||
<th>Actions</th>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var job in Model.Jobs)
|
||
{
|
||
await RenderJobAsync(job, true);
|
||
}
|
||
</tbody>
|
||
</table>
|
||
|
||
<h3>Top delayed deliver job targets</h3>
|
||
<table class="auto-table" id="top-delayed">
|
||
<thead>
|
||
<th class="width0">Instance</th>
|
||
<th>Count</th>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var instance in Model.TopDelayed)
|
||
{
|
||
<tr>
|
||
<td>@instance.Host</td>
|
||
<td><i>@instance.Count jobs</i></td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
</table>
|
||
@if (Model.TopDelayed is [])
|
||
{
|
||
<i>No delayed jobs found.</i>
|
||
}
|
||
|
||
var last = Model.Jobs.FirstOrDefault();
|
||
var lastUpdated = last != null ? new DateTimeOffset(last.LastUpdatedAt).ToUnixTimeMilliseconds() : 0;
|
||
|
||
<div class="display-none" id="last-updated">@lastUpdated</div>
|
||
}
|
||
|
||
else
|
||
{
|
||
var delayedStr = QueueModel.ScheduledQueues.Contains(Model.Queue) ? "scheduled" : "delayed";
|
||
if (Model.Filter == null)
|
||
{
|
||
<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>
|
||
}
|
||
else
|
||
{
|
||
var filterStr = Model.Filter.Value == Job.JobStatus.Delayed && QueueModel.ScheduledQueues.Contains(Model.Queue)
|
||
? "scheduled"
|
||
: Model.Filter.Value.ToString().ToLowerInvariant();
|
||
|
||
<p>
|
||
Listing @Model.TotalCount <span class="status-@filterStr">@filterStr</span> <b>@Model.Queue</b> jobs.
|
||
@if (Model.Filter is Job.JobStatus.Failed)
|
||
{
|
||
<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>
|
||
}
|
||
</p>
|
||
}
|
||
|
||
<table class="auto-table">
|
||
<thead>
|
||
<th class="width0">ID</th>
|
||
<th>Status</th>
|
||
<th>Actions</th>
|
||
</thead>
|
||
<tbody>
|
||
@foreach (var job in Model.Jobs)
|
||
{
|
||
await RenderJobAsync(job);
|
||
}
|
||
</tbody>
|
||
</table>
|
||
|
||
<div class="flex">
|
||
@if (Model.PrevPage != null)
|
||
{
|
||
if (Model.Filter.HasValue)
|
||
{
|
||
<button role="link" data-target="/queue/@Model.Queue/@Model.PrevPage/@Model.Filter.Value.ToString().ToLowerInvariant()" onclick="navigate">❮ Previous page</button>
|
||
}
|
||
else
|
||
{
|
||
<button role="link" data-target="/queue/@Model.Queue/@Model.PrevPage" onclick="navigate(event)">❮ Previous page</button>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<button disabled>❮ Previous page</button>
|
||
}
|
||
|
||
@if (Model.NextPage != null)
|
||
{
|
||
if (Model.Filter.HasValue)
|
||
{
|
||
<button role="link" data-target="/queue/@Model.Queue/@Model.NextPage/@Model.Filter.Value.ToString().ToLowerInvariant()" onclick="navigate(event)">Next page ❯</button>
|
||
}
|
||
else
|
||
{
|
||
<button role="link" data-target="/queue/@Model.Queue/@Model.NextPage" onclick="navigate(event)">Next page ❯</button>
|
||
}
|
||
}
|
||
else
|
||
{
|
||
<button disabled>Next page ❯</button>
|
||
}
|
||
|
||
<select onchange="filter('@Model.Queue')" id="filter" class="inline-flex">
|
||
@if (Model.Filter == null)
|
||
{
|
||
<option value="all" selected>All</option>
|
||
}
|
||
else
|
||
{
|
||
<option value="all">All</option>
|
||
}
|
||
@foreach (var status in Enum.GetValues<Job.JobStatus>())
|
||
{
|
||
var statusStr = status == Job.JobStatus.Delayed && QueueModel.ScheduledQueues.Contains(Model.Queue)
|
||
? "Scheduled"
|
||
: status.ToString();
|
||
|
||
if (Model.Filter.Equals(status))
|
||
{
|
||
<option value="@status.ToString().ToLowerInvariant()" selected>@statusStr</option>
|
||
}
|
||
else
|
||
{
|
||
<option value="@status.ToString().ToLowerInvariant()">@statusStr</option>
|
||
}
|
||
}
|
||
</select>
|
||
|
||
<form onsubmit="return lookupJob(event)" class="inline-flex flex-grow">
|
||
<input type="text" id="lookup" placeholder="Lookup job by id" minlength="36" maxlength="36" class="flex-grow"/>
|
||
</form>
|
||
</div>
|
||
}
|
||
|
||
@{
|
||
async Task RenderJobAsync(Job job, bool withQueue = false)
|
||
{
|
||
var id = job.Id.ToStringLower();
|
||
var additional = job.Status switch
|
||
{
|
||
Job.JobStatus.Delayed when job.RetryCount is 0 => $"for {job.DelayedUntil?.ToLocalTime().ToDisplayStringTz() ?? "<unknown>"}",
|
||
// Separated for readability
|
||
Job.JobStatus.Queued => $"for {job.QueueDuration.ToDurationDisplayString()}",
|
||
Job.JobStatus.Running => $"for {job.Duration.ToDurationDisplayString()}",
|
||
Job.JobStatus.Delayed => $"until {job.DelayedUntil?.ToLocalTime().ToDisplayStringTz() ?? "<unknown>"}",
|
||
Job.JobStatus.Completed => $"at {job.FinishedAt?.ToLocalTime().ToDisplayStringTz() ?? "<unknown>"}",
|
||
Job.JobStatus.Failed => $"at {job.FinishedAt?.ToLocalTime().ToDisplayStringTz() ?? "<unknown>"}",
|
||
_ => throw new ArgumentOutOfRangeException()
|
||
};
|
||
|
||
var classes = Model.Last != null && new DateTimeOffset(job.LastUpdatedAt).ToUnixTimeMilliseconds() > Model.Last
|
||
? "new-item"
|
||
: "";
|
||
|
||
var status = job is { Status: Job.JobStatus.Delayed, RetryCount: 0 } ? "Scheduled" : job.Status.ToString();
|
||
|
||
<tr class="@classes">
|
||
@if (withQueue)
|
||
{
|
||
<td class="uuid-abbrev">@id[..8]...@id[24..]</td>
|
||
<td>@job.Queue</td>
|
||
}
|
||
else
|
||
{
|
||
<td class="uuid">@id</td>
|
||
}
|
||
<td class="status-@status.ToLowerInvariant()">
|
||
<b>@status</b> <small>@additional</small>
|
||
<td>
|
||
<a href="/queue/job/@id">View details</a>
|
||
</td>
|
||
</tr>
|
||
}
|
||
} |