Added small system overview
This commit is contained in:
46
Moonlight.Frontend/Formatter.cs
Normal file
46
Moonlight.Frontend/Formatter.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace Moonlight.Frontend;
|
||||
|
||||
public static class Formatter
|
||||
{
|
||||
public static string FormatSize(long bytes, double conversionStep = 1024)
|
||||
{
|
||||
if (bytes == 0) return "0 B";
|
||||
|
||||
string[] units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
var unitIndex = 0;
|
||||
double size = bytes;
|
||||
|
||||
while (size >= conversionStep && unitIndex < units.Length - 1)
|
||||
{
|
||||
size /= conversionStep;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
var decimals = unitIndex == 0 ? 0 : 2;
|
||||
return $"{Math.Round(size, decimals)} {units[unitIndex]}";
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan timeSpan)
|
||||
{
|
||||
var abs = timeSpan.Duration(); // Handle negative timespans
|
||||
|
||||
if (abs.TotalSeconds < 1)
|
||||
return $"{abs.TotalMilliseconds:F0}ms";
|
||||
|
||||
if (abs.TotalMinutes < 1)
|
||||
return $"{abs.TotalSeconds:F1}s";
|
||||
|
||||
if (abs.TotalHours < 1)
|
||||
return $"{abs.Minutes}m {abs.Seconds}s";
|
||||
|
||||
if (abs.TotalDays < 1)
|
||||
return $"{abs.Hours}h {abs.Minutes}m";
|
||||
|
||||
if (abs.TotalDays < 365)
|
||||
return $"{abs.Days}d {abs.Hours}h";
|
||||
|
||||
var years = (int)(abs.TotalDays / 365);
|
||||
var days = abs.Days % 365;
|
||||
return days > 0 ? $"{years}y {days}d" : $"{years}y";
|
||||
}
|
||||
}
|
||||
156
Moonlight.Frontend/UI/Admin/Views/Overview.razor
Normal file
156
Moonlight.Frontend/UI/Admin/Views/Overview.razor
Normal file
@@ -0,0 +1,156 @@
|
||||
@page "/admin"
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Spinners
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<h1 class="text-xl font-semibold">Overview</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Here you can see a quick overview of your moonlight instance
|
||||
</div>
|
||||
|
||||
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-5">
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>CPU Usage</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Math.Round(InfoResponse.CpuUsage, 2)%</CardTitle>
|
||||
<CardAction>
|
||||
<CpuIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Memory Usage</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Formatter.FormatSize(InfoResponse.MemoryUsage)</CardTitle>
|
||||
<CardAction>
|
||||
<MemoryStickIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Operating System</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@InfoResponse.OperatingSystem</CardTitle>
|
||||
<CardAction>
|
||||
<ComputerIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Uptime</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Formatter.FormatDuration(InfoResponse.Uptime)</CardTitle>
|
||||
<CardAction>
|
||||
<ClockIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Version</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@InfoResponse.VersionName</CardTitle>
|
||||
<CardAction>
|
||||
<RocketIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (IsInfoLoading || InfoResponse == null)
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardHeader>
|
||||
<CardDescription>Update Status</CardDescription>
|
||||
@if (InfoResponse.IsUpToDate)
|
||||
{
|
||||
<CardTitle ClassName="text-lg text-green-500">Up-to-date</CardTitle>
|
||||
<CardAction>
|
||||
<RefreshCwIcon ClassName="size-6 text-muted-foreground" />
|
||||
</CardAction>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
|
||||
<CardAction ClassName="self-center">
|
||||
<Button>Update</Button>
|
||||
</CardAction>
|
||||
}
|
||||
</CardHeader>
|
||||
}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool IsInfoLoading = true;
|
||||
private SystemInfoResponse? InfoResponse;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if(!firstRender)
|
||||
return;
|
||||
|
||||
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoResponse>("api/admin/system/info", Constants.SerializerOptions);
|
||||
IsInfoLoading = false;
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -80,6 +80,14 @@
|
||||
IsExactPath = true
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Overview",
|
||||
IconType = typeof(LayoutDashboardIcon),
|
||||
Path = "/admin",
|
||||
IsExactPath = true,
|
||||
Group = "Admin"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Users",
|
||||
IconType = typeof(UsersRoundIcon),
|
||||
@@ -87,6 +95,14 @@
|
||||
IsExactPath = false,
|
||||
Group = "Admin"
|
||||
},
|
||||
new()
|
||||
{
|
||||
Name = "Settings",
|
||||
IconType = typeof(SettingsIcon),
|
||||
Path = "/admin/settings",
|
||||
IsExactPath = false,
|
||||
Group = "Admin"
|
||||
}
|
||||
]);
|
||||
|
||||
Navigation.LocationChanged += OnLocationChanged;
|
||||
Reference in New Issue
Block a user