Refactored ui. Improved console experience. Added command endpoint

This commit is contained in:
2025-07-18 21:16:52 +02:00
parent f8c11b2dd8
commit 265a4b280b
43 changed files with 479 additions and 149 deletions

View File

@@ -0,0 +1,30 @@
@using MoonlightServers.Frontend.UI.Components.Forms
@using MoonlightServers.Shared.Http.Requests.Admin.Nodes
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-base-content">Enable Transparent
Mode
</label>
<div class="mt-2">
<div class="flex items-center">
<Switch @bind-Value="Request.EnableTransparentMode"/>
</div>
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-base-content">
Enable Dynamic Firewall
</label>
<div class="mt-2">
<div class="flex items-center">
<Switch @bind-Value="Request.EnableDynamicFirewall"/>
</div>
</div>
</div>
</div>
@code
{
[Parameter] public UpdateNodeRequest Request { get; set; }
}

View File

@@ -0,0 +1,145 @@
@using MoonCore.Blazor.FlyonUi.Alerts
@using MoonCore.Blazor.FlyonUi.DataTables
@using MoonCore.Blazor.FlyonUi.Modals
@using MoonCore.Blazor.FlyonUi.Toasts
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes
@using MoonCore.Helpers
@using MoonCore.Models
@using MoonlightServers.Frontend.UI.Components.Nodes.Modals
@using MoonlightServers.Shared.Http.Requests.Admin.NodeAllocations
@using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations
@inject HttpApiClient ApiClient
@inject ModalService ModalService
@inject ToastService ToastService
@inject AlertService AlertService
<div class="grid grid-cols-1 md:grid-cols-3 md:gap-x-5">
<div class="col-span-1">
<div class="card">
<div class="card-header">
<span class="card-title">Actions</span>
</div>
<div class="card-body">
<div class="flex flex-col gap-y-3">
<button type="button" @onclick="AddAllocation" class="btn btn-primary">Create</button>
<button type="button" @onclick="AddAllocationRange" class="btn btn-accent">Create multiple</button>
<button type="button" @onclick="DeleteAllAllocations" class="btn btn-error">Delete all</button>
</div>
</div>
</div>
</div>
<div class="col-span-1 md:col-span-2 -mb-3">
<DataTable @ref="Table" TItem="NodeAllocationResponse">
<Configuration>
<Pagination TItem="NodeAllocationResponse" ItemSource="LoadData"/>
<DataTableColumn TItem="NodeAllocationResponse" Field="@(x => x.IpAddress)" Name="IP Address"/>
<DataTableColumn TItem="NodeAllocationResponse" Field="@(x => x.Port)" Name="Port"/>
<DataTableColumn TItem="NodeAllocationResponse">
<ColumnTemplate>
<div class="flex justify-end items-center">
<a @onclick="() => UpdateAllocation(context)"
@onclick:preventDefault href="#"
class="text-primary mr-2 sm:mr-3">
<i class="icon-pencil text-base"></i>
</a>
<a @onclick="() => DeleteAllocation(context)"
@onclick:preventDefault href="#"
class="text-error">
<i class="icon-trash text-base"></i>
</a>
</div>
</ColumnTemplate>
</DataTableColumn>
</Configuration>
</DataTable>
</div>
</div>
@code
{
[Parameter] public NodeResponse Node { get; set; }
private DataTable<NodeAllocationResponse> Table;
private async Task<IPagedData<NodeAllocationResponse>> LoadData(PaginationOptions options)
{
return await ApiClient.GetJson<PagedData<NodeAllocationResponse>>(
$"api/admin/servers/nodes/{Node.Id}/allocations?page={options.Page}&pageSize={options.PerPage}"
);
}
private async Task AddAllocationRange()
{
Func<CreateNodeAllocationRangeRequest, Task> onSubmit = async request =>
{
await ApiClient.Post($"api/admin/servers/nodes/{Node.Id}/allocations/range", request);
await ToastService.Success("Successfully created allocations");
await Table.Refresh();
};
await ModalService.Launch<CreateMultipleAllocationModal>(parameters => { parameters.Add("OnSubmit", onSubmit); });
}
private async Task AddAllocation()
{
Func<CreateNodeAllocationRequest, Task> onSubmit = async request =>
{
await ApiClient.Post($"api/admin/servers/nodes/{Node.Id}/allocations", request);
await ToastService.Success("Successfully created allocation");
await Table.Refresh();
};
await ModalService.Launch<CreateAllocationModal>(parameters => { parameters.Add("OnSubmit", onSubmit); });
}
private async Task UpdateAllocation(NodeAllocationResponse allocation)
{
Func<UpdateNodeAllocationRequest, Task> onSubmit = async request =>
{
await ApiClient.Patch($"api/admin/servers/nodes/{Node.Id}/allocations/{allocation.Id}", request);
await ToastService.Success("Successfully updated allocation");
await Table.Refresh();
};
await ModalService.Launch<UpdateAllocationModal>(parameters =>
{
parameters.Add("OnSubmit", onSubmit);
parameters.Add("Allocation", allocation);
});
}
private async Task DeleteAllocation(NodeAllocationResponse allocation)
{
await AlertService.ConfirmDanger(
"Delete allocation",
"Do you really want to delete the selected allocation? This cannot be undone",
async () =>
{
await ApiClient.Delete($"api/admin/servers/nodes/{Node.Id}/allocations/{allocation.Id}");
await ToastService.Success("Successfully deleted allocation");
await Table.Refresh();
}
);
}
private async Task DeleteAllAllocations()
{
await AlertService.ConfirmDanger(
"Delete all allocations",
"Do you really want to delete all allocations? This cannot be undone",
async () =>
{
await ApiClient.Delete($"api/admin/servers/nodes/{Node.Id}/allocations/all");
await ToastService.Success("Successfully deleted allocations");
await Table.Refresh();
}
);
}
}

View File

@@ -0,0 +1,32 @@
@using MoonlightServers.Shared.Http.Requests.Admin.Nodes
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-base-content">Name</label>
<div class="mt-2">
<input @bind="Request.Name" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-base-content">Fqdn</label>
<div class="mt-2">
<input @bind="Request.Fqdn" type="text" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2">
<label class="block text-sm font-medium leading-6 text-base-content">Http Port</label>
<div class="mt-2">
<input @bind="Request.HttpPort" type="number" autocomplete="off" class="input w-full">
</div>
</div>
<div class="sm:col-span-2"><label class="block text-sm font-medium leading-6 text-base-content">Ftp Port</label>
<div class="mt-2">
<input @bind="Request.FtpPort" type="number" autocomplete="off" class="input w-full">
</div>
</div>
</div>
@code
{
[Parameter] public UpdateNodeRequest Request { get; set; }
}

View File

@@ -0,0 +1,217 @@
@using Microsoft.Extensions.Logging
@using MoonCore.Blazor.FlyonUi.Components
@using MoonCore.Blazor.FlyonUi.Toasts
@using MoonCore.Helpers
@using MoonlightServers.Frontend.Services
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes.Statistics
@inject NodeService NodeService
@inject ToastService ToastService
@inject ILogger<Overview> Logger
@implements IDisposable
<LazyLoader Load="Load">
<div class="mb-3 mt-5 text-xl font-semibold">
Overview
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div class="col-span-1 card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-base-content">
@(Math.Round(Statistics.Cpu.Usage, 2))%
</p>
<i class="icon-cpu text-4xl text-primary"></i>
</div>
<div class="text-base text-base-content/90">
<span class="truncate">
CPU: @Statistics.Cpu.Model
</span>
</div>
</div>
<div class="col-span-1 card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-base-content">
@(Formatter.FormatSize(Statistics.Memory.Total - Statistics.Memory.Available))
/
@(Formatter.FormatSize(Statistics.Memory.Total))
</p>
<i class="icon-memory-stick text-4xl text-primary"></i>
</div>
<div class="text-base text-base-content/90">
Memory
</div>
</div>
<div class="col-span-1 card card-body">
<div class="flex justify-between">
<div class="text-xl font-semibold text-base-content">
@(Formatter.FormatSize(Statistics.Memory.SwapTotal - Statistics.Memory.SwapFree))
/
@(Formatter.FormatSize(Statistics.Memory.SwapTotal))
</div>
<i class="icon-shapes text-4xl text-primary"></i>
</div>
<div class="text-base text-base-content/90">
Swap
</div>
</div>
</div>
<div class="mb-3 mt-5 text-xl font-semibold">
CPU
</div>
<div class="card card-body grid grid-cols-1 lg:grid-cols-2 gap-y-2 gap-x-5">
@{
var i = 0;
}
@foreach (var usage in Statistics.Cpu.UsagePerCore)
{
var percentRounded = Math.Round(usage, 2);
<div class="flex flex-row items-center col-span-1">
<div class="text-sm text-base-content/90 me-1.5 grow-0 flex flex-col">
<span>#@(i)</span>
</div>
<div class="grow">
<div class="progress h-2" role="progressbar">
<div class="progress-bar transition-all duration-300 ease-in-out @(GetBackgroundColorByPercent(percentRounded))" style="width: @(usage)%"></div>
</div>
</div>
</div>
i++;
}
</div>
<div class="mb-3 mt-5 text-xl font-semibold">
Disks
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
@foreach (var disk in Statistics.Disks)
{
var usedPercent = Math.Round((disk.DiskTotal - disk.DiskFree) / (double)disk.DiskTotal * 100, 2);
var iNodesPercent = Math.Round((disk.InodesTotal - disk.InodesFree) / (double)disk.InodesTotal * 100, 2);
<div class="col-span-1 card card-body">
<div class="flex items-center">
<div class="grow">
<div class="progress h-2" role="progressbar">
<div class="progress-bar transition-all duration-300 ease-in-out @(GetBackgroundColorByPercent(usedPercent))" style="width: @(usedPercent)%"></div>
</div>
</div>
</div>
<div class="text-sm text-base-content/90 mt-2.5 flex flex-col">
<div>
Device: <span class="font-semibold">@disk.Device</span> - Mounted at: <span class="font-semibold truncate">@disk.MountPath</span>
</div>
<div>
Used: <span class="font-semibold">@Formatter.FormatSize(disk.DiskTotal - disk.DiskFree)</span>
Total: <span class="font-semibold">@Formatter.FormatSize(disk.DiskTotal)</span>
</div>
<div>
INodes: <span class="font-semibold">@(iNodesPercent)%</span>
</div>
</div>
</div>
}
</div>
<div class="mb-3 mt-5 text-xl font-semibold">
Docker
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div class="col-span-1 card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-base-content">
@Formatter.FormatSize(DockerStatistics.ImagesUsed) (@Formatter.FormatSize(DockerStatistics.ImagesReclaimable) unused)
</p>
<i class="icon-gallery-horizontal-end text-4xl text-primary"></i>
</div>
<div class="text-base text-base-content/90">
Images
</div>
</div>
<div class="col-span-1 card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-base-content">
@Formatter.FormatSize(DockerStatistics.ContainersUsed) ( @Formatter.FormatSize(DockerStatistics.ContainersReclaimable) unused)
</p>
<i class="icon-container text-4xl text-primary"></i>
</div>
<div class="text-base text-base-content/90">
Containers
</div>
</div>
<div class="col-span-1 card card-body">
<div class="flex justify-between">
<p class="text-xl font-semibold text-base-content">
@Formatter.FormatSize(DockerStatistics.BuildCacheUsed) (@Formatter.FormatSize(DockerStatistics.BuildCacheReclaimable) unused)
</p>
<i class="icon-hard-hat text-4xl text-primary"></i>
</div>
<div class="text-base text-base-content/90">
Build Cache
</div>
</div>
</div>
</LazyLoader>
@code
{
[Parameter] public NodeResponse Node { get; set; }
private StatisticsResponse Statistics;
private DockerStatisticsResponse DockerStatistics;
private Timer? UpdateTimer;
private async Task Load(LazyLoader _)
{
Statistics = await NodeService.GetStatistics(Node.Id);
DockerStatistics = await NodeService.GetDockerStatistics(Node.Id);
UpdateTimer = new Timer(HandleUpdate, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(3));
}
private async void HandleUpdate(object? _)
{
try
{
Statistics = await NodeService.GetStatistics(Node.Id);
DockerStatistics = await NodeService.GetDockerStatistics(Node.Id);
await InvokeAsync(StateHasChanged);
}
catch (Exception e)
{
Logger.LogWarning("An error occured while fetching status update: {e}", e);
await ToastService.Error("Unable to fetch status update", e.Message);
}
}
private string GetBackgroundColorByPercent(double percent)
{
if (percent < 70)
return "bg-success";
else if (percent < 80)
return "bg-warning";
else
return "bg-error";
}
public void Dispose()
{
UpdateTimer?.Dispose();
}
}