301 lines
10 KiB
Plaintext
301 lines
10 KiB
Plaintext
@using LucideBlazor
|
|
@using MoonlightServers.Frontend.Infrastructure.Helpers
|
|
@using MoonlightServers.Frontend.Shared
|
|
@using MoonlightServers.Shared
|
|
@using MoonlightServers.Shared.Admin.Nodes
|
|
@using ShadcnBlazor.Cards
|
|
@using ShadcnBlazor.Emptys
|
|
@using ShadcnBlazor.Spinners
|
|
@using ShadcnBlazor.Progresses
|
|
|
|
@inject HttpClient HttpClient
|
|
|
|
@implements IAsyncDisposable
|
|
|
|
<h3 class="text-base font-semibold mt-5 mb-2">Overview</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
|
<div class="col-span-1">
|
|
<RealtimeChart @ref="CpuChart" T="double"
|
|
Title="CPU Usage"
|
|
DisplayField="@(d => $"{Math.Round(d, 2)}%")"
|
|
ValueField="d => d"
|
|
ClassName="h-32"/>
|
|
</div>
|
|
|
|
<div class="col-span-1">
|
|
<RealtimeChart @ref="MemoryChart" T="MemoryDataPoint"
|
|
Title="Memory Usage"
|
|
DisplayField="@(d => Formatter.FormatSize(d.UsedMemory))"
|
|
ValueField="d => d.Percent"
|
|
ClassName="h-32"/>
|
|
</div>
|
|
|
|
<div class="col-span-1">
|
|
<RealtimeChart @ref="NetworkInChart" T="long"
|
|
Title="Incoming Traffic"
|
|
DisplayField="@(d => $"{Formatter.FormatSize(d)}/s")"
|
|
Min="0"
|
|
Max="512"
|
|
ValueField="@(d => d / 1024f / 1024f)"
|
|
ClassName="h-32"/>
|
|
</div>
|
|
|
|
<div class="col-span-1">
|
|
<RealtimeChart @ref="NetworkOutChart" T="long"
|
|
Title="Outgoing Traffic"
|
|
DisplayField="@(d => $"{Formatter.FormatSize(d)}/s")"
|
|
Min="0"
|
|
Max="512"
|
|
ValueField="@(d => d / 1024f / 1024f)"
|
|
ClassName="h-32"/>
|
|
</div>
|
|
|
|
<Card ClassName="col-span-1">
|
|
@if (HasLoaded && StatisticsDto != null)
|
|
{
|
|
<CardHeader ClassName="gap-0">
|
|
<CardDescription>Uptime</CardDescription>
|
|
<CardTitle ClassName="text-lg">@Formatter.FormatDuration(StatisticsDto.Uptime)</CardTitle>
|
|
</CardHeader>
|
|
}
|
|
else
|
|
{
|
|
<CardContent ClassName="flex justify-center items-center">
|
|
<Spinner ClassName="size-8"/>
|
|
</CardContent>
|
|
}
|
|
</Card>
|
|
|
|
<Card ClassName="col-span-1">
|
|
@if (HasLoaded && HealthDto != null)
|
|
{
|
|
<CardHeader ClassName="gap-0">
|
|
<CardDescription>Health Status</CardDescription>
|
|
<CardTitle ClassName="text-lg">
|
|
@if (HealthDto.IsHealthy)
|
|
{
|
|
<span class="text-green-500">Healthy</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="text-destructive">Unhealthy</span>
|
|
}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
}
|
|
else
|
|
{
|
|
<CardContent ClassName="flex justify-center items-center">
|
|
<Spinner ClassName="size-8"/>
|
|
</CardContent>
|
|
}
|
|
</Card>
|
|
</div>
|
|
|
|
<h3 class="text-base font-semibold mt-8 mb-2">Details</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
|
|
|
<Card ClassName="col-span-1">
|
|
<CardHeader>
|
|
<CardTitle>System Details</CardTitle>
|
|
<CardDescription>Details over your general system configuration</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
@if (StatisticsDto == null)
|
|
{
|
|
<Empty>
|
|
<EmptyHeader>
|
|
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
|
<CpuIcon/>
|
|
</EmptyMedia>
|
|
<EmptyTitle>No details</EmptyTitle>
|
|
<EmptyDescription>
|
|
No details about your system found
|
|
</EmptyDescription>
|
|
</EmptyHeader>
|
|
</Empty>
|
|
}
|
|
else
|
|
{
|
|
<div class="space-y-3">
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-muted-foreground font-medium">CPU Model</span>
|
|
<span>
|
|
@StatisticsDto.Cpu.ModelName
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-muted-foreground font-medium">Total Memory</span>
|
|
<span>
|
|
@Formatter.FormatSize(StatisticsDto.Memory.TotalBytes)
|
|
</span>
|
|
</div>
|
|
<div class="flex justify-between text-sm">
|
|
<span class="text-muted-foreground font-medium">Total Disk Space</span>
|
|
<span>
|
|
@{
|
|
var totalDiskSpace = StatisticsDto
|
|
.Disks
|
|
.DistinctBy(x => x.Device)
|
|
.Sum(x => x.TotalBytes);
|
|
}
|
|
|
|
@Formatter.FormatSize(totalDiskSpace)
|
|
</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card ClassName="col-span-1">
|
|
<CardHeader>
|
|
<CardTitle>Disk Details</CardTitle>
|
|
<CardDescription>Details over all your mounted disks</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
@if (StatisticsDto == null)
|
|
{
|
|
<Empty>
|
|
<EmptyHeader>
|
|
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
|
<HardDriveIcon/>
|
|
</EmptyMedia>
|
|
<EmptyTitle>No details</EmptyTitle>
|
|
<EmptyDescription>
|
|
No details about disk and their usage found
|
|
</EmptyDescription>
|
|
</EmptyHeader>
|
|
</Empty>
|
|
}
|
|
else
|
|
{
|
|
<div class="space-y-4">
|
|
@foreach (var disk in StatisticsDto.Disks)
|
|
{
|
|
<div class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium">
|
|
@disk.MountPoint
|
|
</span>
|
|
<span class="text-xs text-muted-foreground">
|
|
@disk.FileSystem
|
|
</span>
|
|
<span class="text-xs text-muted-foreground">
|
|
@disk.Device
|
|
</span>
|
|
</div>
|
|
<span class="text-sm text-muted-foreground">
|
|
@Formatter.FormatSize(disk.UsedBytes) / @Formatter.FormatSize(disk.TotalBytes)
|
|
</span>
|
|
</div>
|
|
|
|
<Progress Value="(int)disk.UsedPercent" Max="100"></Progress>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
@code
|
|
{
|
|
[Parameter] public NodeDto Node { get; set; }
|
|
|
|
private NodeHealthDto? HealthDto;
|
|
private NodeStatisticsDto? StatisticsDto;
|
|
|
|
private bool HasLoaded;
|
|
|
|
private RealtimeChart<double>? CpuChart;
|
|
|
|
private RealtimeChart<long>? NetworkInChart;
|
|
|
|
private RealtimeChart<long>? NetworkOutChart;
|
|
|
|
private RealtimeChart<MemoryDataPoint>? MemoryChart;
|
|
private Timer? Timer;
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (!firstRender) return;
|
|
|
|
HealthDto = await HttpClient.GetFromJsonAsync<NodeHealthDto>(
|
|
$"api/admin/servers/nodes/{Node.Id}/health",
|
|
SerializationContext.Default.Options
|
|
);
|
|
|
|
if (HealthDto is { IsHealthy: true })
|
|
{
|
|
StatisticsDto = await HttpClient.GetFromJsonAsync<NodeStatisticsDto>(
|
|
$"api/admin/servers/nodes/{Node.Id}/statistics",
|
|
SerializationContext.Default.Options
|
|
);
|
|
}
|
|
|
|
HasLoaded = true;
|
|
await InvokeAsync(StateHasChanged);
|
|
|
|
Timer = new Timer(RefreshCallbackAsync, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
|
}
|
|
|
|
private async void RefreshCallbackAsync(object? state)
|
|
{
|
|
try
|
|
{
|
|
StatisticsDto = await HttpClient.GetFromJsonAsync<NodeStatisticsDto>(
|
|
$"api/admin/servers/nodes/{Node.Id}/statistics",
|
|
SerializationContext.Default.Options
|
|
);
|
|
|
|
if (StatisticsDto == null) return;
|
|
|
|
if (CpuChart != null)
|
|
await CpuChart.PushAsync(StatisticsDto.Cpu.TotalUsagePercent);
|
|
|
|
if (MemoryChart != null)
|
|
await MemoryChart.PushAsync(new MemoryDataPoint(StatisticsDto.Memory.UsedBytes, StatisticsDto.Memory.UsedPercent));
|
|
|
|
if (NetworkInChart != null && NetworkOutChart != null)
|
|
{
|
|
var networkInterface = StatisticsDto
|
|
.Network
|
|
.FirstOrDefault(x => x.Name.StartsWith("eth"));
|
|
|
|
if (networkInterface == null)
|
|
{
|
|
networkInterface = StatisticsDto
|
|
.Network
|
|
.FirstOrDefault(x => x.Name.StartsWith("en"));
|
|
}
|
|
|
|
if (networkInterface == null)
|
|
return;
|
|
|
|
await NetworkInChart.PushAsync(networkInterface.RxBytesPerSec);
|
|
await NetworkOutChart.PushAsync(networkInterface.TxBytesPerSec);
|
|
}
|
|
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine(e);
|
|
}
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
if (Timer != null)
|
|
await Timer.DisposeAsync();
|
|
|
|
if (CpuChart != null)
|
|
await CpuChart.DisposeAsync();
|
|
}
|
|
|
|
private record MemoryDataPoint(long UsedMemory, double Percent);
|
|
} |