Modifiche stacked bar chart
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace EgwCoreLib.Lux.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper gestione colori RGB(A)
|
||||
/// </summary>
|
||||
public static class ColorHelper
|
||||
{
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Imposta un valore di alpha specifico. Se la stringa non è valida, ritorna un fallback nero.
|
||||
/// </summary>
|
||||
/// <param name="color">Stringa tipo "rgb(0,0,0)" o "rgba(0,0,0,0.5)"</param>
|
||||
/// <param name="alpha">Valore da 0.0 a 1.0</param>
|
||||
public static string SetAlpha(string color, double alpha)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(color)) return $"rgba(0,0,0,{alpha.ToString(CultureInfo.InvariantCulture)})";
|
||||
|
||||
// Cerchiamo i componenti R, G, B
|
||||
var match = RgbComponentsRegex.Match(color);
|
||||
|
||||
if (match.Success)
|
||||
{
|
||||
// Estraiamo i primi 3 gruppi (R, G, B)
|
||||
string r = match.Groups[1].Value;
|
||||
string g = match.Groups[2].Value;
|
||||
string b = match.Groups[3].Value;
|
||||
|
||||
// Usiamo InvariantCulture per assicurarci che il punto decimale sia sempre '.' e non ','
|
||||
string alphaStr = alpha.ToString("0.0", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"rgba({r},{g},{b},{alphaStr})";
|
||||
}
|
||||
|
||||
// Se la stringa non è in formato rgb/rgba (magari è un nome colore o hex),
|
||||
// per ora restituiamo l'originale o potresti gestire l'errore.
|
||||
return color;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Forza l'alpha a 1, indipendentemente dal formato originale (rgb o rgba).
|
||||
/// </summary>
|
||||
public static string ToOpaqueRgba(string color) => SetAlpha(color, 1.0);
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Private Fields
|
||||
|
||||
// Pattern per catturare i tre componenti R, G, B ignorando l'eventuale A esistente
|
||||
private static readonly Regex RgbComponentsRegex = new Regex(@"rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)", RegexOptions.Compiled);
|
||||
|
||||
#endregion Private Fields
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Version>0.9.2512.1812</Version>
|
||||
<Version>0.9.2512.1815</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,113 +1 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
@implements IDisposable
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Dictionary<string, double> PieDict { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string, string> PieColor { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public string CanvasId { get; set; } = "pieChart";
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "#";
|
||||
|
||||
private bool created = false;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
// if (firstRender && PieDict.Any())
|
||||
if (firstRender || !created)
|
||||
{
|
||||
created = true;
|
||||
pieLabels = PieDict.Keys.ToList();
|
||||
pieValues = PieDict.Values.ToList();
|
||||
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
|
||||
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, new
|
||||
{
|
||||
type = "pie",
|
||||
data = new
|
||||
{
|
||||
labels = pieLabels,
|
||||
datasets = new[]
|
||||
{
|
||||
new {
|
||||
label = Label,
|
||||
data = pieValues,
|
||||
backgroundColor= bgColors,
|
||||
borderColor= bordColors,
|
||||
borderWidth= 2
|
||||
}
|
||||
}
|
||||
},
|
||||
options = new
|
||||
{
|
||||
responsive = true,
|
||||
animation = false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
public void Dispose()
|
||||
{
|
||||
// Chiama il metodo JS per pulire il dizionario
|
||||
_ = JSRuntime.InvokeVoidAsync("chartsInterop.destroyChart", CanvasId);
|
||||
}
|
||||
private List<string> pieLabels = new();
|
||||
private List<double> pieValues = new();
|
||||
private List<string> bgColors = new();
|
||||
private List<string> bordColors = new();
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (created)
|
||||
{
|
||||
|
||||
|
||||
pieLabels = PieDict.Keys.ToList();
|
||||
pieValues = PieDict.Values.ToList();
|
||||
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
|
||||
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
|
||||
|
||||
// await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, pieLabels, values, bgColors);
|
||||
|
||||
// await JSRuntime.InvokeVoidAsync("chartsInterop.createChart", CanvasId, new
|
||||
await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, new
|
||||
{
|
||||
type = "pie",
|
||||
data = new
|
||||
{
|
||||
labels = pieLabels,
|
||||
datasets = new[]
|
||||
{
|
||||
new {
|
||||
label = Label,
|
||||
data = pieValues,
|
||||
backgroundColor= bgColors,
|
||||
borderColor= bordColors,
|
||||
borderWidth= 2
|
||||
}
|
||||
}
|
||||
},
|
||||
options = new
|
||||
{
|
||||
responsive = true,
|
||||
animation = false
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
pieLabels = PieDict.Keys.ToList();
|
||||
pieValues = PieDict.Values.ToList();
|
||||
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
|
||||
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<canvas id="@CanvasId"></canvas>
|
||||
<canvas id="@CanvasId"></canvas>
|
||||
@@ -0,0 +1,138 @@
|
||||
using EgwCoreLib.Lux.Core;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace Lux.UI.Components.Compo.Charts
|
||||
{
|
||||
public partial class PieChart : IDisposable
|
||||
{
|
||||
#region Public Properties
|
||||
|
||||
[Parameter]
|
||||
public bool Animate { get; set; } = false;
|
||||
|
||||
[Parameter]
|
||||
public string CanvasId { get; set; } = "pieChart";
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = "#";
|
||||
|
||||
/// <summary>
|
||||
/// Lista colori in formato RGBA
|
||||
/// es: rgba(0,0,0,0.3)
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public Dictionary<string, string> PieColor { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string, double> PieDict { get; set; } = new();
|
||||
|
||||
[Parameter]
|
||||
public bool Responsive { get; set; } = true;
|
||||
|
||||
[Parameter]
|
||||
public double Alpha { get; set; } = 0.5;
|
||||
|
||||
#endregion Public Properties
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Chiama il metodo JS per pulire il dizionario
|
||||
_ = JSRuntime.InvokeVoidAsync("chartsInterop.destroyChart", CanvasId);
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
#region Protected Properties
|
||||
|
||||
[Inject]
|
||||
protected IJSRuntime JSRuntime { get; set; } = null!;
|
||||
|
||||
#endregion Protected Properties
|
||||
|
||||
#region Protected Methods
|
||||
|
||||
/// <summary>
|
||||
/// Setup dati per plotting
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
// solo se ho tutti i dati...
|
||||
if (PieDict != null && PieDict.Count > 0)
|
||||
{
|
||||
await DoRenderChart();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Protected Methods
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private List<string> bgColors = new();
|
||||
|
||||
private List<string> bordColors = new();
|
||||
|
||||
private List<string> pieLabels = new();
|
||||
|
||||
private List<double> pieValues = new();
|
||||
|
||||
#endregion Private Fields
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Conversione/trasformazione dati + chiamata js x rendering
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private async Task DoRenderChart()
|
||||
{
|
||||
pieLabels = PieDict.Keys.ToList();
|
||||
pieValues = PieDict.Values.ToList();
|
||||
#if false
|
||||
bgColors = pieLabels.Select(l => PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0.3)").ToList();
|
||||
bordColors = bgColors.Select(c => c.Replace("0.3", "1")).ToList();
|
||||
#endif
|
||||
// Sfondo: usiamo SetAlpha per garantire che, qualunque sia il colore nel dizionario, l'alpha sia Alpha
|
||||
bgColors = pieLabels
|
||||
.Select(l => {
|
||||
string baseColor = PieColor.ContainsKey(l) ? PieColor[l] : "rgba(0,0,0,0)";
|
||||
return ColorHelper.SetAlpha(baseColor, Alpha);
|
||||
})
|
||||
.ToList();
|
||||
|
||||
// Bordo: partiamo dai bgColors e forziamo l'opacità totale
|
||||
bordColors = bgColors
|
||||
.Select(c => ColorHelper.ToOpaqueRgba(c))
|
||||
.ToList();
|
||||
|
||||
await JSRuntime.InvokeVoidAsync("chartsInterop.createPieChart", CanvasId, new
|
||||
{
|
||||
type = "pie",
|
||||
data = new
|
||||
{
|
||||
labels = pieLabels,
|
||||
datasets = new[]
|
||||
{
|
||||
new {
|
||||
label = Label,
|
||||
data = pieValues,
|
||||
backgroundColor= bgColors,
|
||||
borderColor= bordColors,
|
||||
borderWidth= 2
|
||||
}
|
||||
}
|
||||
},
|
||||
options = new
|
||||
{
|
||||
responsive = Responsive,
|
||||
animation = Animate
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
@inject IJSRuntime JSRuntime
|
||||
|
||||
<canvas id="@CanvasId"></canvas>
|
||||
@code {
|
||||
[Parameter] public Dictionary<string, string> BarColor { get; set; } = new();
|
||||
[Parameter] public List<string> ListX { get; set; } = new(); // puoi passare DateTime.ToString()
|
||||
@@ -8,20 +9,45 @@
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && DictY.Any())
|
||||
// if (firstRender && DictY.Any())
|
||||
if (DictY.Any())
|
||||
{
|
||||
var datasets = DictY.Select(kvp => new
|
||||
{
|
||||
label = kvp.Key,
|
||||
data = kvp.Value,
|
||||
backgroundColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,0.3)",
|
||||
borderColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,1)",
|
||||
// Usiamo il tuo nuovo ColorHelper!
|
||||
backgroundColor = ColorHelper.SetAlpha(BarColor.GetValueOrDefault(kvp.Key, "rgba(0,0,0,0.3)"), 0.3),
|
||||
borderColor = ColorHelper.ToOpaqueRgba(BarColor.GetValueOrDefault(kvp.Key, "rgba(0,0,0,1)")),
|
||||
// backgroundColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,0.3)",
|
||||
// borderColor = BarColor.ContainsKey(kvp.Key) ? BarColor[kvp.Key] : "rgba(0,0,0,1)",
|
||||
borderWidth = 2
|
||||
}).ToList();
|
||||
|
||||
#if false
|
||||
await JSRuntime.InvokeVoidAsync("chartsInterop.createBarChart", CanvasId, ListX, datasets);
|
||||
#endif
|
||||
|
||||
// Passiamo l'intera configurazione per gestire lo 'stacked'
|
||||
await JSRuntime.InvokeVoidAsync("chartsInterop.createBarChart", CanvasId, new
|
||||
{
|
||||
type = "bar",
|
||||
data = new
|
||||
{
|
||||
labels = ListX,
|
||||
datasets = datasets
|
||||
},
|
||||
options = new
|
||||
{
|
||||
responsive = true,
|
||||
scales = new
|
||||
{
|
||||
x = new { stacked = true }, // Abilita lo stack su X
|
||||
y = new { stacked = true } // Abilita lo stack su Y
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<canvas id="@CanvasId"></canvas>
|
||||
|
||||
@@ -9,20 +9,20 @@
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<PieChart CanvasId="pieEventCount" PieDict="@PieCount" PieColor="@PieColors" Label="# Richieste" />
|
||||
<PieChart CanvasId="pieEventCount" PieDict="@DatasetCount" PieColor="@PieColors" Label="# Richieste" />
|
||||
|
||||
</div>
|
||||
<div class="col-10">
|
||||
@* <StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@SetColors" /> *@
|
||||
<StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@PieColors" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-2">
|
||||
<PieChart CanvasId="pieElapsed" PieDict="@PieElapsed" PieColor="@PieColors" Label="Elapsed"/>
|
||||
<PieChart CanvasId="pieElapsed" PieDict="@DataseElapsed" PieColor="@PieColors" Label="Elapsed"/>
|
||||
|
||||
</div>
|
||||
<div class="col-10">
|
||||
@* <StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@SetColors" /> *@
|
||||
@* <StackedBarChart CanvasId="barEventCount" ListX="@ListX" DictY="@DictY" BarColor="@PieColors" /> *@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
@@ -37,9 +37,6 @@ namespace Lux.UI.Components.Compo.Stats
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
#if false
|
||||
InitConfig();
|
||||
#endif
|
||||
await InitCharts();
|
||||
_cts = new CancellationTokenSource();
|
||||
_ = UpdateLoop(_cts.Token);
|
||||
@@ -140,37 +137,17 @@ namespace Lux.UI.Components.Compo.Stats
|
||||
{
|
||||
PieColors.Clear();
|
||||
PieColors.Add("WINDOW", "rgba(54,162,235,0.6)");
|
||||
PieColors.Add("BEAM", "rgba(255,99,132,0.6)");
|
||||
#if false
|
||||
Labels.Clear();
|
||||
Labels.Add("WINDOW");
|
||||
Labels.Add("BEAM");
|
||||
|
||||
|
||||
PieCount.Clear();
|
||||
PieCount.Add("WINDOW", 208);
|
||||
PieCount.Add("BEAM", 13);
|
||||
|
||||
ListX.Clear();
|
||||
ListX = new List<string> { "2025-12-17T00:00:00" };
|
||||
|
||||
DictY.Clear();
|
||||
DictY.Add("WINDOW", new List<decimal> { 208, 150 });
|
||||
DictY.Add("BEAM", new List<decimal> { 13, 20 });
|
||||
#endif
|
||||
PieCount.Clear();
|
||||
PieCount.Add("NONE", 0);
|
||||
PieElapsed.Clear();
|
||||
PieElapsed.Add("NONE", 0);
|
||||
PieColors.Add("BEAM", "rgba(69,195,132,0.6)");
|
||||
|
||||
DatasetCount.Clear();
|
||||
DatasetCount.Add("NONE", 0);
|
||||
DataseElapsed.Clear();
|
||||
DataseElapsed.Add("NONE", 0);
|
||||
}
|
||||
|
||||
private Dictionary<string, string> PieColors = new Dictionary<string, string>();
|
||||
private Dictionary<string, double> PieCount = new Dictionary<string, double>();
|
||||
private Dictionary<string, double> PieElapsed = new Dictionary<string, double>();
|
||||
#if false
|
||||
private List<string> Labels = new List<string>();
|
||||
#endif
|
||||
private Dictionary<string, double> DatasetCount = new Dictionary<string, double>();
|
||||
private Dictionary<string, double> DataseElapsed = new Dictionary<string, double>();
|
||||
private List<string> ListX = new List<string>();
|
||||
private Dictionary<string, List<decimal>> DictY = new Dictionary<string, List<decimal>>();
|
||||
|
||||
@@ -220,27 +197,45 @@ namespace Lux.UI.Components.Compo.Stats
|
||||
// calcolo i valori da mostrare...
|
||||
if (rtProcStats != null && rtProcStats.Count > 0)
|
||||
{
|
||||
#if false
|
||||
|
||||
PieCount.Clear();
|
||||
PieElapsed.Clear();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
#endif
|
||||
|
||||
PieCount = rtProcStats
|
||||
DatasetCount = rtProcStats
|
||||
.GroupBy(p => p.TagClass)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => (double)g.Sum(p => p.EventCount)
|
||||
);
|
||||
|
||||
PieElapsed = rtProcStats
|
||||
DataseElapsed = rtProcStats
|
||||
.GroupBy(p => p.TagClass)
|
||||
.ToDictionary(
|
||||
g => g.Key,
|
||||
g => g.Sum(p => p.Elapsed)
|
||||
);
|
||||
|
||||
|
||||
// 1. Definiamo le etichette dell'asse X (es. le ultime 24 ore o quelle presenti nei dati)
|
||||
ListX = rtProcStats
|
||||
.Select(p => p.Hour.ToString("HH:mm"))
|
||||
.Distinct()
|
||||
.OrderBy(t => t)
|
||||
.ToList();
|
||||
|
||||
// 2. Identifichiamo tutti i TagClass unici (saranno i nostri dataset)
|
||||
var allTags = rtProcStats.Select(p => p.TagClass).Distinct().ToList();
|
||||
|
||||
// 3. Costruiamo il DictY: per ogni Tag, creiamo una lista di valori allineata a ListX
|
||||
DictY = allTags.ToDictionary(
|
||||
tag => tag,
|
||||
tag => ListX.Select(hour =>
|
||||
{
|
||||
// Cerchiamo il valore per quel tag in quell'ora specifica
|
||||
return (decimal)(rtProcStats
|
||||
.FirstOrDefault(p => p.TagClass == tag && p.Hour.ToString("HH:mm") == hour)
|
||||
?.EventCount ?? 0); // Se non esiste, mettiamo 0
|
||||
}).ToList()
|
||||
);
|
||||
|
||||
// forzatura update
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,20 +76,31 @@
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
createBarChart: function (canvasId, labels, datasets) {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: { labels: labels, datasets: datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: { legend: { position: "top" } },
|
||||
scales: {
|
||||
x: { stacked: true, title: { display: true, text: "X Axis" } },
|
||||
y: { stacked: true, title: { display: true, text: "Values" } }
|
||||
}
|
||||
}
|
||||
});
|
||||
createBarChart: function (canvasId, config) {
|
||||
if (this.charts[canvasId]) {
|
||||
const chart = this.charts[canvasId];
|
||||
chart.data.labels = config.data.labels;
|
||||
chart.data.datasets = config.data.datasets; // Sostituisce i dataset con i nuovi
|
||||
chart.update('none'); // Update senza animazione per aggiornamenti real-time
|
||||
} else {
|
||||
const ctx = document.getElementById(canvasId).getContext('2d');
|
||||
this.charts[canvasId] = new Chart(ctx, config);
|
||||
}
|
||||
}
|
||||
|
||||
//createBarChart: function (canvasId, labels, datasets) {
|
||||
// const ctx = document.getElementById(canvasId);
|
||||
// new Chart(ctx, {
|
||||
// type: "bar",
|
||||
// data: { labels: labels, datasets: datasets },
|
||||
// options: {
|
||||
// responsive: true,
|
||||
// plugins: { legend: { position: "top" } },
|
||||
// scales: {
|
||||
// x: { stacked: true, title: { display: true, text: "X Axis" } },
|
||||
// y: { stacked: true, title: { display: true, text: "Values" } }
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
//}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<body>
|
||||
<i>LUX - Web Windows MES</i>
|
||||
<h4>Versione: 0.9.2512.1812</h4>
|
||||
<h4>Versione: 0.9.2512.1815</h4>
|
||||
<br /> Note di rilascio:
|
||||
<ul>
|
||||
<li>
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.9.2512.1812
|
||||
0.9.2512.1815
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<item>
|
||||
<version>0.9.2512.1812</version>
|
||||
<version>0.9.2512.1815</version>
|
||||
<url>http://nexus.steamware.net/repository/SWS/GPW/stable/GPW.UI.zip</url>
|
||||
<changelog>http://nexus.steamware.net/repository/SWS/GPW/stable/ChangeLog.html</changelog>
|
||||
<mandatory>false</mandatory>
|
||||
|
||||
Reference in New Issue
Block a user