mirror of
https://github.com/aaru-dps/Aaru.Server.git
synced 2025-12-16 19:24:27 +00:00
Render all statistics tables.
This commit is contained in:
@@ -8,6 +8,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Blazorise.Bootstrap" Version="1.5.2"/>
|
||||||
|
<PackageReference Include="Blazorise.DataGrid" Version="1.5.2"/>
|
||||||
|
<PackageReference Include="Blazorise.Icons.FontAwesome" Version="1.5.2"/>
|
||||||
<PackageReference Include="Markdig" Version="0.37.0"/>
|
<PackageReference Include="Markdig" Version="0.37.0"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.4"/>
|
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="8.0.4"/>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.4"/>
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="8.0.4"/>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<!DOCTYPE html>
|
@using Blazorise
|
||||||
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<base href="/"/>
|
<base href="/"/>
|
||||||
<link href="bootstrap/bootstrap.min.css" rel="stylesheet"/>
|
<link crossorigin="anonymous" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" rel="stylesheet">
|
||||||
|
<link href="_content/Blazorise.Icons.FontAwesome/v6/css/all.min.css" rel="stylesheet">
|
||||||
|
<link href="_content/Blazorise/blazorise.css" rel="stylesheet"/>
|
||||||
|
<link href="_content/Blazorise.Bootstrap/blazorise.bootstrap.css" rel="stylesheet"/>
|
||||||
<link href="css/prism.css" rel="stylesheet"/>
|
<link href="css/prism.css" rel="stylesheet"/>
|
||||||
<link href="app.css" rel="stylesheet"/>
|
<link href="app.css" rel="stylesheet"/>
|
||||||
<link href="Aaru.Server.New.styles.css" rel="stylesheet"/>
|
<link href="Aaru.Server.New.styles.css" rel="stylesheet"/>
|
||||||
@@ -14,9 +18,22 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<Routes/>
|
<Blazorise.ThemeProvider Theme="@theme">
|
||||||
|
<Routes/>
|
||||||
|
</Blazorise.ThemeProvider>
|
||||||
<script src="_framework/blazor.web.js"></script>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
<script src="scripts/prism.js"></script>
|
<script src="scripts/prism.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
@code{
|
||||||
|
private readonly Theme theme = new()
|
||||||
|
{
|
||||||
|
ColorOptions = new ThemeColorOptions
|
||||||
|
{
|
||||||
|
Dark = ThemeColors.Gray.Shades["100"].Value,
|
||||||
|
Light = ThemeColors.Gray.Shades["800"].Value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,6 +1,134 @@
|
|||||||
@page "/Stats"
|
@page "/Stats"
|
||||||
|
@using Aaru.CommonTypes.Metadata
|
||||||
@using Aaru.Server.Database
|
@using Aaru.Server.Database
|
||||||
|
@using Aaru.Server.Database.Models
|
||||||
|
@using Blazorise.DataGrid
|
||||||
|
|
||||||
@inject Microsoft.EntityFrameworkCore.IDbContextFactory<DbContext> DbContextFactory
|
@inject Microsoft.EntityFrameworkCore.IDbContextFactory<DbContext> DbContextFactory
|
||||||
|
|
||||||
<PageTitle>Aaru: Statistics</PageTitle>
|
<PageTitle>Aaru: Statistics</PageTitle>
|
||||||
|
|
||||||
|
@*
|
||||||
|
TODO: Group by datagrid
|
||||||
|
TODO: Sortable datagrid
|
||||||
|
*@
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All operating systems Aaru has run on...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@OperatingSystems" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="NameValueStats">
|
||||||
|
<DataGridColumn Caption="Name" Field="@nameof(NameValueStats.name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times" Field="@nameof(NameValueStats.Value)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All Aaru versions...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@Versions" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="NameValueStats">
|
||||||
|
<DataGridColumn Caption="Version" Field="@nameof(NameValueStats.name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times run" Field="@nameof(NameValueStats.Value)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All Aaru commands...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@Commands" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="Command">
|
||||||
|
<DataGridColumn Caption="Command" Field="@nameof(Command.Name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times run" Field="@nameof(Command.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All filters found...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@Filters" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="Filter">
|
||||||
|
<DataGridColumn Caption="Filter" Field="@nameof(Filter.Name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times found" Field="@nameof(Filter.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All media image formats found...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@MediaImages" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="MediaFormat">
|
||||||
|
<DataGridColumn Caption="Media image format" Field="@nameof(MediaFormat.Name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times found" Field="@nameof(MediaFormat.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All partitioning schemes found...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@Partitions" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="Partition">
|
||||||
|
<DataGridColumn Caption="Partitioning scheme" Field="@nameof(Partition.Name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times found" Field="@nameof(Partition.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All filesystems found...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@Filesystems" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="Filesystem">
|
||||||
|
<DataGridColumn Caption="Filesystem name" Field="@nameof(Filesystem.Name)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times found" Field="@nameof(Filesystem.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All media types found in images...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@VirtualMedia" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="MediaItem">
|
||||||
|
<DataGridColumn Caption="Physical type" Field="@nameof(MediaItem.Type)"/>
|
||||||
|
<DataGridColumn Caption="Logical type" Field="@nameof(MediaItem.SubType)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times found" Field="@nameof(MediaItem.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All media types found in devices...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@RealMedia" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="MediaItem">
|
||||||
|
<DataGridColumn Caption="Physical type" Field="@nameof(MediaItem.Type)"/>
|
||||||
|
<DataGridColumn Caption="Logical type" Field="@nameof(MediaItem.SubType)"/>
|
||||||
|
<DataGridNumericColumn Caption="Times found" Field="@nameof(MediaItem.Count)"/>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-section">
|
||||||
|
<h1>
|
||||||
|
<p class="text-center" style="color: deeppink;">All devices found...</p>
|
||||||
|
</h1>
|
||||||
|
<DataGrid Data="@Devices" FixedHeader FixedHeaderDataGridMaxHeight="300px" PageSize="int.MaxValue" TItem="DeviceItem">
|
||||||
|
<DataGridColumn Caption="Manufacturer" Field="@nameof(DeviceItem.Manufacturer)"/>
|
||||||
|
<DataGridColumn Caption="Model" Field="@nameof(DeviceItem.Model)"/>
|
||||||
|
<DataGridColumn Caption="Revision" Field="@nameof(DeviceItem.Revision)"/>
|
||||||
|
<DataGridColumn Caption="Bus" Field="@nameof(DeviceItem.Bus)"/>
|
||||||
|
<DataGridColumn Caption="Report" Field="@nameof(DeviceItem.ReportId)">
|
||||||
|
<DisplayTemplate>
|
||||||
|
@{
|
||||||
|
int? reportId = context?.ReportId;
|
||||||
|
|
||||||
|
if(reportId > 0)
|
||||||
|
{
|
||||||
|
<a href="/report/@reportId.Value" target="_blank">Yes</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>No</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</DisplayTemplate>
|
||||||
|
</DataGridColumn>
|
||||||
|
</DataGrid>
|
||||||
|
</div>
|
||||||
@@ -9,6 +9,23 @@ namespace Aaru.Server.New.Components.Pages;
|
|||||||
|
|
||||||
public partial class Stats
|
public partial class Stats
|
||||||
{
|
{
|
||||||
|
List<NameValueStats> OperatingSystems { get; set; } = [];
|
||||||
|
|
||||||
|
List<NameValueStats> Versions { get; set; } = [];
|
||||||
|
|
||||||
|
List<Command> Commands { get; set; } = [];
|
||||||
|
|
||||||
|
List<Filter> Filters { get; set; } = [];
|
||||||
|
|
||||||
|
List<MediaFormat> MediaImages { get; set; } = [];
|
||||||
|
|
||||||
|
List<Partition> Partitions { get; set; } = [];
|
||||||
|
|
||||||
|
List<Filesystem> Filesystems { get; set; } = [];
|
||||||
|
List<MediaItem> RealMedia { get; set; } = [];
|
||||||
|
List<MediaItem> VirtualMedia { get; set; } = [];
|
||||||
|
List<DeviceItem> Devices { get; set; } = [];
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -17,38 +34,37 @@ public partial class Stats
|
|||||||
// TOOD: Cache real OS name in database, lookups would be much faster
|
// TOOD: Cache real OS name in database, lookups would be much faster
|
||||||
await using DbContext _ctx = await DbContextFactory.CreateDbContextAsync();
|
await using DbContext _ctx = await DbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
var operatingSystems = (await _ctx.OperatingSystems.OrderBy(static os => os.Name)
|
OperatingSystems = (await _ctx.OperatingSystems.OrderBy(static os => os.Name)
|
||||||
.ThenBy(static os => os.Version)
|
.ThenBy(static os => os.Version)
|
||||||
.Select(static nvs => new NameValueStats
|
.Select(static nvs => new NameValueStats
|
||||||
{
|
{
|
||||||
name =
|
name =
|
||||||
$"{GetPlatformName(nvs.Name, nvs.Version)}{(string.IsNullOrEmpty(nvs.Version) ? "" : " ")}{nvs.Version}",
|
$"{GetPlatformName(nvs.Name, nvs.Version)}{(string.IsNullOrEmpty(nvs.Version) ? "" : " ")}{nvs.Version}",
|
||||||
Value = nvs.Count
|
Value = nvs.Count
|
||||||
})
|
})
|
||||||
.ToListAsync()).OrderBy(static os => os.name)
|
.ToListAsync()).OrderBy(static os => os.name)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var versions = (await _ctx.Versions.Select(static nvs => new NameValueStats
|
Versions = (await _ctx.Versions.Select(static nvs => new NameValueStats
|
||||||
{
|
{
|
||||||
name = nvs.Name == "previous" ? "Previous than 3.4.99.0" : nvs.Name,
|
name = nvs.Name == "previous" ? "Previous than 3.4.99.0" : nvs.Name,
|
||||||
Value = nvs.Count
|
Value = nvs.Count
|
||||||
})
|
})
|
||||||
.ToListAsync()).OrderBy(static version => version.name)
|
.ToListAsync()).OrderBy(static version => version.name)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
List<Command> commands = await _ctx.Commands.OrderBy(static c => c.Name).ToListAsync();
|
Commands = await _ctx.Commands.OrderBy(static c => c.Name).ToListAsync();
|
||||||
|
|
||||||
List<Filter> filters = await _ctx.Filters.OrderBy(static filter => filter.Name).ToListAsync();
|
Filters = await _ctx.Filters.OrderBy(static filter => filter.Name).ToListAsync();
|
||||||
|
|
||||||
List<MediaFormat> mediaImages = await _ctx.MediaFormats.OrderBy(static format => format.Name).ToListAsync();
|
MediaImages = await _ctx.MediaFormats.OrderBy(static format => format.Name).ToListAsync();
|
||||||
|
|
||||||
List<Partition> partitions = await _ctx.Partitions.OrderBy(static partition => partition.Name).ToListAsync();
|
Partitions = await _ctx.Partitions.OrderBy(static partition => partition.Name).ToListAsync();
|
||||||
|
|
||||||
List<Filesystem> filesystems =
|
Filesystems = await _ctx.Filesystems.OrderBy(static filesystem => filesystem.Name).ToListAsync();
|
||||||
await _ctx.Filesystems.OrderBy(static filesystem => filesystem.Name).ToListAsync();
|
|
||||||
|
|
||||||
List<MediaItem> realMedia = [];
|
RealMedia = [];
|
||||||
List<MediaItem> virtualMedia = [];
|
VirtualMedia = [];
|
||||||
|
|
||||||
await foreach(Media nvs in _ctx.Medias.AsAsyncEnumerable())
|
await foreach(Media nvs in _ctx.Medias.AsAsyncEnumerable())
|
||||||
{
|
{
|
||||||
@@ -60,7 +76,7 @@ public partial class Stats
|
|||||||
|
|
||||||
if(nvs.Real)
|
if(nvs.Real)
|
||||||
{
|
{
|
||||||
realMedia.Add(new MediaItem
|
RealMedia.Add(new MediaItem
|
||||||
{
|
{
|
||||||
Type = mediaType.type,
|
Type = mediaType.type,
|
||||||
SubType = mediaType.subType,
|
SubType = mediaType.subType,
|
||||||
@@ -69,7 +85,7 @@ public partial class Stats
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
virtualMedia.Add(new MediaItem
|
VirtualMedia.Add(new MediaItem
|
||||||
{
|
{
|
||||||
Type = mediaType.type,
|
Type = mediaType.type,
|
||||||
SubType = mediaType.subType,
|
SubType = mediaType.subType,
|
||||||
@@ -81,7 +97,7 @@ public partial class Stats
|
|||||||
{
|
{
|
||||||
if(nvs.Real)
|
if(nvs.Real)
|
||||||
{
|
{
|
||||||
realMedia.Add(new MediaItem
|
RealMedia.Add(new MediaItem
|
||||||
{
|
{
|
||||||
Type = nvs.Type,
|
Type = nvs.Type,
|
||||||
SubType = null,
|
SubType = null,
|
||||||
@@ -90,7 +106,7 @@ public partial class Stats
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
virtualMedia.Add(new MediaItem
|
VirtualMedia.Add(new MediaItem
|
||||||
{
|
{
|
||||||
Type = nvs.Type,
|
Type = nvs.Type,
|
||||||
SubType = null,
|
SubType = null,
|
||||||
@@ -100,24 +116,22 @@ public partial class Stats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
realMedia = realMedia.OrderBy(static media => media.Type).ThenBy(static media => media.SubType).ToList();
|
RealMedia = RealMedia.OrderBy(static media => media.Type).ThenBy(static media => media.SubType).ToList();
|
||||||
virtualMedia = virtualMedia.OrderBy(static media => media.Type).ThenBy(static media => media.SubType).ToList();
|
VirtualMedia = VirtualMedia.OrderBy(static media => media.Type).ThenBy(static media => media.SubType).ToList();
|
||||||
|
|
||||||
|
|
||||||
List<DeviceItem> devices = await _ctx.DeviceStats.Include(static deviceStat => deviceStat.Report)
|
Devices = await _ctx.DeviceStats.Include(static deviceStat => deviceStat.Report)
|
||||||
.Select(static device => new DeviceItem
|
.Select(static device => new DeviceItem
|
||||||
{
|
{
|
||||||
Manufacturer = device.Manufacturer,
|
Manufacturer = device.Manufacturer,
|
||||||
Model = device.Model,
|
Model = device.Model,
|
||||||
Revision = device.Revision,
|
Revision = device.Revision,
|
||||||
Bus = device.Bus,
|
Bus = device.Bus,
|
||||||
ReportId = device.Report != null && device.Report.Id != 0
|
ReportId = device.Report != null && device.Report.Id != 0 ? device.Report.Id : 0
|
||||||
? device.Report.Id
|
})
|
||||||
: 0
|
.ToListAsync();
|
||||||
})
|
|
||||||
.ToListAsync();
|
|
||||||
|
|
||||||
devices = devices.OrderBy(static device => device.Manufacturer)
|
Devices = Devices.OrderBy(static device => device.Manufacturer)
|
||||||
.ThenBy(static device => device.Model)
|
.ThenBy(static device => device.Model)
|
||||||
.ThenBy(static device => device.Revision)
|
.ThenBy(static device => device.Revision)
|
||||||
.ThenBy(static device => device.Bus)
|
.ThenBy(static device => device.Bus)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
using Aaru.Server.New.Components;
|
using Aaru.Server.New.Components;
|
||||||
using Aaru.Server.New.Components.Account;
|
using Aaru.Server.New.Components.Account;
|
||||||
|
using Blazorise;
|
||||||
|
using Blazorise.Bootstrap;
|
||||||
|
using Blazorise.Icons.FontAwesome;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -43,6 +46,8 @@ builder.Services.AddIdentityCore<IdentityUser>(options =>
|
|||||||
|
|
||||||
builder.Services.AddSingleton<IEmailSender<IdentityUser>, IdentityNoOpEmailSender>();
|
builder.Services.AddSingleton<IEmailSender<IdentityUser>, IdentityNoOpEmailSender>();
|
||||||
|
|
||||||
|
builder.Services.AddBlazorise(options => { options.Immediate = true; }).AddBootstrapProviders().AddFontAwesomeIcons();
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
// Configure the HTTP request pipeline.
|
||||||
|
|||||||
@@ -697,3 +697,22 @@ kbd {
|
|||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
color: #DEDEDE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-section {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 30px 15px 40px !important;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-fixed-header .table thead:not(.table-thead-theme) th {
|
||||||
|
background-color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-fixed-header {
|
||||||
|
scrollbar-color: #888888 #333333;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user