Files
decePubClient/Components/Admin/StatisticsPanel.razor
T

209 lines
6.7 KiB
Plaintext
Raw Normal View History

@inherits LocalizableComponentBase
@* PrivaPub's federation statistics (/clientapi/admin/statistics): the last days at a glance, the servers exchanged with
(by traffic, failures, latency or name, 50 a page) and how far the crawler got. Counted per server, never per account. *@
<div class="flex flex-col gap-4 w-full" data-statistics>
<div class="flex flex-wrap gap-2 items-end">
<label class="flex flex-col gap-1 text-sm">
<span>@Localizer["Period"]</span>
<DSelect TValue="int" Size="DSize.Sm" Value="days" ValueChanged="ChangeDays" ValueExpression="() => days">
@foreach (var period in Periods)
{
<option value="@period">@Localizer["{0} days", period]</option>
}
</DSelect>
</label>
<label class="flex flex-col gap-1 text-sm">
<span>@Localizer["Servers by"]</span>
<DSelect TValue="string" Size="DSize.Sm" Value="sort" ValueChanged="ChangeSort" ValueExpression="() => sort">
<option value="traffic">@Localizer["Traffic"]</option>
<option value="failures">@Localizer["Failures"]</option>
<option value="latency">@Localizer["Latency"]</option>
<option value="host">@Localizer["Name"]</option>
</DSelect>
</label>
</div>
@if (IsLoading)
{
<p class="flex w-full items-center justify-center gap-2">
<DLoading Variant="DLoadingVariant.Dots" LabelText="@Localizer["loading"]" />
@Localizer["loading"]
</p>
}
else if (overview is null)
{
<p class="text-sm opacity-70">@Localizer["The statistics could not be read."]</p>
}
else
{
<DStats class="w-full overflow-x-auto neo-raised neo-xs" data-statistics-overview data-scroll-x>
<DStat>
<Heading>@Localizer["Deliveries that arrived"]</Heading>
<ChildContent>@(overview.DeliverySuccess is { } success ? $"{success * 100:0.#}%" : "—")</ChildContent>
<Desc>@Localizer["p50 {0} ms, p95 {1} ms", Ms(overview.DeliveryLatencyMs.P50), Ms(overview.DeliveryLatencyMs.P95)]</Desc>
</DStat>
<DStat>
<Heading><span class="capitalize">@Localizer["servers"]</span></Heading>
<ChildContent>@overview.Hosts.Active</ChildContent>
<Desc>@Localizer["{0} known", overview.Hosts.Known]</Desc>
</DStat>
<DStat>
<Heading>@Localizer["Accounts seen"]</Heading>
<ChildContent>@overview.Accounts</ChildContent>
<Desc>@Localizer["ledger {0} written, {1} dropped", overview.Ledger.Written, overview.Ledger.Dropped]</Desc>
</DStat>
</DStats>
<div class="flex flex-wrap gap-4 text-xs">
@foreach (var (title, counts) in Outcomes)
{
<div class="flex flex-col gap-1 min-w-40">
<b class="text-sm">@title</b>
@foreach (var (outcome, count) in counts.OrderByDescending(c => c.Value))
{
<span class="flex justify-between gap-4 tabular-nums"><span>@outcome</span><span>@count</span></span>
}
</div>
}
</div>
@if (hosts is not null)
{
<DTable Size="DSize.Xs" Zebra="true" WrapperClass="max-h-[28rem] rounded-xl neo-inset neo-2xs" data-statistics-hosts>
<thead>
<tr>
<th>@Localizer["server"]</th>
<th>@Localizer["software"]</th>
<th class="text-right">@Localizer["In"]</th>
<th class="text-right">@Localizer["Out"]</th>
<th class="text-right">@Localizer["Refused"]</th>
<th class="text-right">@Localizer["Failures"]</th>
<th class="text-right">p50</th>
<th>@Localizer["Delivery"]</th>
</tr>
</thead>
<tbody>
@foreach (var host in hosts.Hosts)
{
<tr data-statistics-host="@host.Host">
<td class="max-w-48 truncate">@host.Host</td>
<td>@host.Software @host.Version</td>
<td class="text-right tabular-nums">@host.In</td>
<td class="text-right tabular-nums">@host.Out</td>
<td class="text-right tabular-nums">@host.Refused</td>
<td class="text-right tabular-nums">@host.Failures</td>
<td class="text-right tabular-nums">@Ms(host.LatencyP50)</td>
<td>
@if (host.Delivery?.UnavailableUntil is { } until && until > DateTime.UtcNow)
{
<DBadge Size="DSize.Xs" Color="DColor.Error" title="@until.ToLocalTime().ToString(SUtility.dateTimeFormat)">@Localizer["paused"]</DBadge>
}
else if (host.Delivery?.ConsecutiveFailures > 0)
{
<DBadge Size="DSize.Xs" Color="DColor.Warning">@Localizer["{0} failing", host.Delivery.ConsecutiveFailures]</DBadge>
}
</td>
</tr>
}
</tbody>
</DTable>
@if (hosts.Total > PageSize)
{
<DPagination Size="DSize.Xs" Count="@((hosts.Total + PageSize - 1) / PageSize)" Current="page" CurrentChanged="ChangePage"
PreviousAriaLabel="@Localizer["Previous page"]" NextAriaLabel="@Localizer["Next page"]" />
}
<p class="text-xs opacity-70">@hosts.Attribution</p>
}
@if (crawler is not null)
{
<div class="flex flex-col gap-1 text-xs" data-statistics-crawler>
<b class="text-sm">@Localizer["Crawler"]</b>
<span>
@(crawler.Enabled ? Localizer["on"] : Localizer["off"]) ·
@Localizer["{0} servers touched, {1} only crawled, {2} due, {3} refuse robots", crawler.Touched, crawler.Crawled, crawler.Due, crawler.RobotsDisallowed]
</span>
@foreach (var host in crawler.Recent)
{
<span class="flex flex-wrap gap-2">
<span class="truncate max-w-48">@host.Host</span>
<span class="opacity-70">@host.Software</span>
@if (host.LastCrawledAt is { } crawled)
{
<span class="opacity-70">@crawled.GetPassedTime(Localizer)</span>
}
@if (host.CrawlError is { Length: > 0 } crawlError)
{
<span class="text-error">@crawlError</span>
}
</span>
}
</div>
}
}
</div>
@code {
[Inject] AdminService AdminService { get; set; }
const int PageSize = 50;
static readonly int[] Periods = [1, 7, 30, 90];
ViewStatisticsOverview overview;
ViewHostsPage hosts;
ViewCrawler crawler;
int days = 30;
string sort = "traffic";
int page = 1;
(string Title, Dictionary<string, long> Counts)[] Outcomes =>
[
(Localizer["Received"], overview.Inbound),
(Localizer["Delivered"], overview.Outbound),
(Localizer["Channels"], overview.Channels)
];
protected override async Task OnInitializedAsync()
{
if (CascadingState.IsOffline)
{
WhenOnline(LoadData);
StartOnlinePolling();
IsLoading = false;
return;
}
await LoadData();
IsLoading = false;
}
async Task LoadData()
{
overview = await AdminService.Overview(days);
hosts = await AdminService.Hosts(days, sort, page);
crawler = await AdminService.Crawler();
}
static string Ms(int? value) => value?.ToString() ?? "—";
async Task ChangeDays(int value)
{
days = value;
page = 1;
await LoadData();
}
async Task ChangeSort(string value)
{
sort = value;
page = 1;
hosts = await AdminService.Hosts(days, sort, page);
}
async Task ChangePage(int value)
{
page = value;
hosts = await AdminService.Hosts(days, sort, page);
}
}