feat(app): add value formatting, state converters, and sparkline control
This commit is contained in:
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();
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user