feat(app): add value formatting, state converters, and sparkline control

This commit is contained in:
dualfroz committed 2026-08-26 14:19:19 +02:00
1 parent 1562acf8dd
commit 2648a9a0a6
3 files changed
+168

No files matched your search

@@ -0,0 +1,89 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace Vantage.App.Controls;
public sealed class SparklineControl : Control
{
public static readonly StyledProperty<IReadOnlyList<double>?> ValuesProperty =
AvaloniaProperty.Register<SparklineControl, IReadOnlyList<double>?>(nameof(Values));
public static readonly StyledProperty<double> MaximumProperty =
AvaloniaProperty.Register<SparklineControl, double>(nameof(Maximum), 100);
public static readonly StyledProperty<IBrush> StrokeProperty =
AvaloniaProperty.Register<SparklineControl, IBrush>(nameof(Stroke), Brushes.DeepSkyBlue);
public static readonly StyledProperty<IBrush?> FillProperty =
AvaloniaProperty.Register<SparklineControl, IBrush?>(nameof(Fill));
static SparklineControl()
{
AffectsRender<SparklineControl>(ValuesProperty, MaximumProperty, StrokeProperty, FillProperty);
}
public IReadOnlyList<double>? Values
{
get => GetValue(ValuesProperty);
set => SetValue(ValuesProperty, value);
}
public double Maximum
{
get => GetValue(MaximumProperty);
set => SetValue(MaximumProperty, value);
}
public IBrush Stroke
{
get => GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
public IBrush? Fill
{
get => GetValue(FillProperty);
set => SetValue(FillProperty, value);
}
public override void Render(DrawingContext context)
{
var values = Values;
var width = Bounds.Width;
var height = Bounds.Height;
if (values is null || values.Count < 2 || width <= 0 || height <= 0)
return;
var max = Maximum <= 0 ? 1 : Maximum;
var stepX = width / (values.Count - 1);
var geometry = new StreamGeometry();
using (var ctx = geometry.Open())
{
var first = new Point(0, ToY(values[0], max, height));
ctx.BeginFigure(first, isFilled: Fill is not null);
for (var i = 1; i < values.Count; i++)
ctx.LineTo(new Point(i * stepX, ToY(values[i], max, height)));
if (Fill is not null)
{
ctx.LineTo(new Point((values.Count - 1) * stepX, height));
ctx.LineTo(new Point(0, height));
}
ctx.EndFigure(Fill is not null);
}
if (Fill is not null)
context.DrawGeometry(Fill, null, geometry);
context.DrawGeometry(null, new Pen(Stroke, 1.5), geometry);
}
private static double ToY(double value, double max, double height)
{
var clamped = Math.Clamp(value, 0, max);
return height - clamped / max * height;
}
}
@@ -0,0 +1,43 @@
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media;
using Vantage.Core.Models;
namespace Vantage.App.Converters;
public sealed class HealthStatusToBrushConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) => value switch
{
HealthStatus.Up => Brushes.LimeGreen,
HealthStatus.Degraded => new SolidColorBrush(Color.Parse("#FFB300")),
HealthStatus.Down => new SolidColorBrush(Color.Parse("#EF5350")),
_ => new SolidColorBrush(Color.Parse("#77808B"))
};
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
public sealed class BoolToBrushConverter : IValueConverter
{
public IBrush TrueBrush { get; init; } = Brushes.LimeGreen;
public IBrush FalseBrush { get; init; } = Brushes.Gray;
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
value is true ? TrueBrush : FalseBrush;
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
public static class StateBrushConverters
{
public static readonly BoolToBrushConverter RunningBrush = new()
{
TrueBrush = new SolidColorBrush(Color.Parse("#4CAF50")),
FalseBrush = new SolidColorBrush(Color.Parse("#77808B"))
};
public static readonly HealthStatusToBrushConverter HealthBrush = new();
}
+36
View File
@@ -0,0 +1,36 @@
using System.Globalization;
namespace Vantage.App;
public static class ValueFormatting
{
private static readonly string[] ByteUnits = { "B", "KiB", "MiB", "GiB", "TiB", "PiB" };
public static string Bytes(long bytes)
{
if (bytes <= 0)
return "0 B";
double value = bytes;
var unit = 0;
while (value >= 1024 && unit < ByteUnits.Length - 1)
{
value /= 1024;
unit++;
}
var format = value >= 100 || unit == 0 ? "0" : "0.0";
return value.ToString(format, CultureInfo.InvariantCulture) + " " + ByteUnits[unit];
}
public static string BytesPair(long used, long total) => $"{Bytes(used)} / {Bytes(total)}";
public static string Uptime(TimeSpan uptime)
{
if (uptime.TotalDays >= 1)
return $"{(int)uptime.TotalDays}d {uptime.Hours}h {uptime.Minutes}m";
if (uptime.TotalHours >= 1)
return $"{uptime.Hours}h {uptime.Minutes}m";
return $"{uptime.Minutes}m {uptime.Seconds}s";
}
}