Added ml daemon communication and node stats
This commit is contained in:
53
Moonlight/App/Helpers/DaemonApiHelper.cs
Normal file
53
Moonlight/App/Helpers/DaemonApiHelper.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Moonlight.App.Database.Entities;
|
||||||
|
using Moonlight.App.Exceptions;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using RestSharp;
|
||||||
|
|
||||||
|
namespace Moonlight.App.Helpers;
|
||||||
|
|
||||||
|
public class DaemonApiHelper
|
||||||
|
{
|
||||||
|
private readonly RestClient Client;
|
||||||
|
|
||||||
|
public DaemonApiHelper()
|
||||||
|
{
|
||||||
|
Client = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetApiUrl(Node node)
|
||||||
|
{
|
||||||
|
if(node.Ssl)
|
||||||
|
return $"https://{node.Fqdn}:{node.MoonlightDaemonPort}/";
|
||||||
|
else
|
||||||
|
return $"http://{node.Fqdn}:{node.MoonlightDaemonPort}/";
|
||||||
|
//return $"https://{node.Fqdn}:{node.HttpPort}/";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<T> Get<T>(Node node, string resource)
|
||||||
|
{
|
||||||
|
RestRequest request = new(GetApiUrl(node) + resource);
|
||||||
|
|
||||||
|
request.AddHeader("Content-Type", "application/json");
|
||||||
|
request.AddHeader("Accept", "application/json");
|
||||||
|
request.AddHeader("Authorization", node.Token);
|
||||||
|
|
||||||
|
var response = await Client.GetAsync(request);
|
||||||
|
|
||||||
|
if (!response.IsSuccessful)
|
||||||
|
{
|
||||||
|
if (response.StatusCode != 0)
|
||||||
|
{
|
||||||
|
throw new WingsException(
|
||||||
|
$"An error occured: ({response.StatusCode}) {response.Content}",
|
||||||
|
(int)response.StatusCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"An internal error occured: {response.ErrorMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonConvert.DeserializeObject<T>(response.Content!)!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,4 +88,25 @@ public static class Formatter
|
|||||||
|
|
||||||
return $"{i2s(e.Day)}.{i2s(e.Month)}.{e.Year}";
|
return $"{i2s(e.Day)}.{i2s(e.Month)}.{e.Year}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string FormatSize(double bytes)
|
||||||
|
{
|
||||||
|
var i = Math.Abs(bytes) / 1024D;
|
||||||
|
if (i < 1)
|
||||||
|
{
|
||||||
|
return bytes + " B";
|
||||||
|
}
|
||||||
|
else if (i / 1024D < 1)
|
||||||
|
{
|
||||||
|
return i.Round(2) + " KB";
|
||||||
|
}
|
||||||
|
else if (i / (1024D * 1024D) < 1)
|
||||||
|
{
|
||||||
|
return (i / 1024D).Round(2) + " MB";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return (i / (1024D * 1024D)).Round(2) + " GB";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
15
Moonlight/App/Models/Daemon/Resources/ContainerStats.cs
Normal file
15
Moonlight/App/Models/Daemon/Resources/ContainerStats.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Moonlight.App.Models.Daemon.Resources;
|
||||||
|
|
||||||
|
public class ContainerStats
|
||||||
|
{
|
||||||
|
public List<Container> Containers { get; set; } = new();
|
||||||
|
|
||||||
|
public class Container
|
||||||
|
{
|
||||||
|
public Guid Name { get; set; }
|
||||||
|
public long Memory { get; set; }
|
||||||
|
public double Cpu { get; set; }
|
||||||
|
public long NetworkIn { get; set; }
|
||||||
|
public long NetworkOut { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Moonlight/App/Models/Daemon/Resources/CpuStats.cs
Normal file
8
Moonlight/App/Models/Daemon/Resources/CpuStats.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Moonlight.App.Models.Daemon.Resources;
|
||||||
|
|
||||||
|
public class CpuStats
|
||||||
|
{
|
||||||
|
public double Usage { get; set; }
|
||||||
|
public int Cores { get; set; }
|
||||||
|
public string Model { get; set; } = "";
|
||||||
|
}
|
||||||
9
Moonlight/App/Models/Daemon/Resources/DiskStats.cs
Normal file
9
Moonlight/App/Models/Daemon/Resources/DiskStats.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Moonlight.App.Models.Daemon.Resources;
|
||||||
|
|
||||||
|
public class DiskStats
|
||||||
|
{
|
||||||
|
public long FreeBytes { get; set; }
|
||||||
|
public string DriveFormat { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public long TotalSize { get; set; }
|
||||||
|
}
|
||||||
15
Moonlight/App/Models/Daemon/Resources/MemoryStats.cs
Normal file
15
Moonlight/App/Models/Daemon/Resources/MemoryStats.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace Moonlight.App.Models.Daemon.Resources;
|
||||||
|
|
||||||
|
public class MemoryStats
|
||||||
|
{
|
||||||
|
public List<MemoryStick> Sticks { get; set; } = new();
|
||||||
|
public double Free { get; set; }
|
||||||
|
public double Used { get; set; }
|
||||||
|
public double Total { get; set; }
|
||||||
|
|
||||||
|
public class MemoryStick
|
||||||
|
{
|
||||||
|
public int Size { get; set; }
|
||||||
|
public string Type { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.App.Models.Node;
|
|
||||||
|
|
||||||
public class CpuStats
|
|
||||||
{
|
|
||||||
public double CpuUsage { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
namespace Moonlight.App.Models.Node;
|
|
||||||
|
|
||||||
public class DiskStats
|
|
||||||
{
|
|
||||||
public long FreeBytes { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
namespace Moonlight.App.Models.Node;
|
|
||||||
|
|
||||||
public class DockerStats
|
|
||||||
{
|
|
||||||
public ContainerStats[] Containers { get; set; }
|
|
||||||
public int NodeId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ContainerStats
|
|
||||||
{
|
|
||||||
public Guid Name { get; set; }
|
|
||||||
public long Memory { get; set; }
|
|
||||||
public double Cpu { get; set; }
|
|
||||||
public long NetworkIn { get; set; }
|
|
||||||
public long NetworkOut { get; set; }
|
|
||||||
public int NodeId { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace Moonlight.App.Models.Node;
|
|
||||||
|
|
||||||
public class MemoryStats
|
|
||||||
{
|
|
||||||
public long Free { get; set; }
|
|
||||||
public long Used { get; set; }
|
|
||||||
public long Total { get; set; }
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using Moonlight.App.Database.Entities;
|
using Moonlight.App.Database.Entities;
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
|
using Moonlight.App.Models.Daemon.Resources;
|
||||||
using Moonlight.App.Models.Wings.Resources;
|
using Moonlight.App.Models.Wings.Resources;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
|
|
||||||
@@ -7,17 +8,37 @@ namespace Moonlight.App.Services;
|
|||||||
|
|
||||||
public class NodeService
|
public class NodeService
|
||||||
{
|
{
|
||||||
private readonly NodeRepository NodeRepository;
|
|
||||||
private readonly WingsApiHelper WingsApiHelper;
|
private readonly WingsApiHelper WingsApiHelper;
|
||||||
|
private readonly DaemonApiHelper DaemonApiHelper;
|
||||||
|
|
||||||
public NodeService(NodeRepository nodeRepository, WingsApiHelper wingsApiHelper)
|
public NodeService(WingsApiHelper wingsApiHelper, DaemonApiHelper daemonApiHelper)
|
||||||
{
|
{
|
||||||
NodeRepository = nodeRepository;
|
|
||||||
WingsApiHelper = wingsApiHelper;
|
WingsApiHelper = wingsApiHelper;
|
||||||
|
DaemonApiHelper = daemonApiHelper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SystemStatus> GetStatus(Node node)
|
public async Task<SystemStatus> GetStatus(Node node)
|
||||||
{
|
{
|
||||||
return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
|
return await WingsApiHelper.Get<SystemStatus>(node, "api/system");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CpuStats> GetCpuStats(Node node)
|
||||||
|
{
|
||||||
|
return await DaemonApiHelper.Get<CpuStats>(node, "stats/cpu");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<MemoryStats> GetMemoryStats(Node node)
|
||||||
|
{
|
||||||
|
return await DaemonApiHelper.Get<MemoryStats>(node, "stats/memory");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<DiskStats> GetDiskStats(Node node)
|
||||||
|
{
|
||||||
|
return await DaemonApiHelper.Get<DiskStats>(node, "stats/disk");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ContainerStats> GetContainerStats(Node node)
|
||||||
|
{
|
||||||
|
return await DaemonApiHelper.Get<ContainerStats>(node, "stats/container");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="App\Http\Middleware" />
|
<Folder Include="App\Http\Middleware" />
|
||||||
<Folder Include="App\Models\AuditLogData" />
|
<Folder Include="App\Models\Daemon\Requests" />
|
||||||
<Folder Include="App\Models\Google\Resources" />
|
<Folder Include="App\Models\Google\Resources" />
|
||||||
<Folder Include="App\Services\DiscordBot\Modules" />
|
<Folder Include="App\Services\DiscordBot\Modules" />
|
||||||
<Folder Include="resources\lang" />
|
<Folder Include="resources\lang" />
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ namespace Moonlight
|
|||||||
builder.Services.AddScoped<WingsConsoleHelper>();
|
builder.Services.AddScoped<WingsConsoleHelper>();
|
||||||
builder.Services.AddSingleton<PaperApiHelper>();
|
builder.Services.AddSingleton<PaperApiHelper>();
|
||||||
builder.Services.AddSingleton<HostSystemHelper>();
|
builder.Services.AddSingleton<HostSystemHelper>();
|
||||||
|
builder.Services.AddScoped<DaemonApiHelper>();
|
||||||
|
|
||||||
// Background services
|
// Background services
|
||||||
builder.Services.AddSingleton<DiscordBotService>();
|
builder.Services.AddSingleton<DiscordBotService>();
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
@page "/admin/nodes"
|
@page "/admin/nodes"
|
||||||
@using Moonlight.App.Repositories
|
@using Moonlight.App.Repositories
|
||||||
@using Moonlight.App.Database.Entities
|
@using Moonlight.App.Database.Entities
|
||||||
@using Moonlight.App.Helpers
|
|
||||||
@using Moonlight.App.Models.Node
|
|
||||||
@using Moonlight.App.Models.Wings.Resources
|
@using Moonlight.App.Models.Wings.Resources
|
||||||
@using Moonlight.App.Services
|
@using Moonlight.App.Services
|
||||||
@using Moonlight.App.Services.Interop
|
@using Moonlight.App.Services.Interop
|
||||||
@using Logging.Net
|
@using Logging.Net
|
||||||
|
@using BlazorTable
|
||||||
|
|
||||||
@inject NodeRepository NodeRepository
|
@inject NodeRepository NodeRepository
|
||||||
@inject AlertService AlertService
|
@inject AlertService AlertService
|
||||||
@@ -14,47 +13,32 @@
|
|||||||
@inject SmartTranslateService SmartTranslateService
|
@inject SmartTranslateService SmartTranslateService
|
||||||
|
|
||||||
<OnlyAdmin>
|
<OnlyAdmin>
|
||||||
<div class="row mb-5">
|
<div class="row">
|
||||||
<div class="card card-body">
|
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||||
<a class="btn btn-primary" href="/admin/nodes/new">
|
<div class="card">
|
||||||
<TL>Add a new node</TL>
|
<div class="card-header border-0 pt-5">
|
||||||
|
<h3 class="card-title align-items-start flex-column">
|
||||||
|
<span class="card-label fw-bold fs-3 mb-1">
|
||||||
|
<TL>Nodes</TL>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<div class="card-toolbar">
|
||||||
|
<a href="/admin/nodes/new" class="btn btn-sm btn-light-success">
|
||||||
|
<i class="bx bx-layer-plus"></i>
|
||||||
|
<TL>New node</TL>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="card-body pt-0">
|
||||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
@if (Nodes.Any())
|
||||||
@if (!Nodes.Any())
|
|
||||||
{
|
{
|
||||||
<div class="card card-body">
|
<div class="table-responsive">
|
||||||
<div class="alert alert-info">
|
<Table TableItem="Node" Items="Nodes" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
<TL>No nodes found. Start with adding a new node</TL>
|
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||||
</div>
|
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Status"))" Field="@(x => x.Id)" Sortable="true" Filterable="true">
|
||||||
</div>
|
<Template>
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
foreach (var node in Nodes)
|
|
||||||
{
|
|
||||||
<div class="col-xl-6 mb-xl-10">
|
|
||||||
<div class="card card-flush h-xl-100">
|
|
||||||
<div class="card-header pt-5">
|
|
||||||
<h4 class="card-title d-flex align-items-start flex-column">
|
|
||||||
<span class="card-label fw-bold text-gray-800">@(node.Name) - (ID: @(node.Id))</span>
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body pt-6">
|
|
||||||
<div class="d-flex flex-stack">
|
|
||||||
<div class="d-flex align-items-center me-3">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<span class="text-gray-800 fs-5 fw-bold lh-0">
|
|
||||||
<TL>Status</TL>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center w-100 mw-125px">
|
|
||||||
<span class="text-white-400 fw-semibold">
|
|
||||||
@{
|
@{
|
||||||
var ss = StatusCache.ContainsKey(node) ? StatusCache[node] : null;
|
var ss = StatusCache.ContainsKey(context) ? StatusCache[context] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (ss == null)
|
@if (ss == null)
|
||||||
@@ -65,117 +49,49 @@
|
|||||||
{
|
{
|
||||||
<span class="text-success">Online (@(ss.Version))</span>
|
<span class="text-success">Online (@(ss.Version))</span>
|
||||||
}
|
}
|
||||||
</span>
|
</Template>
|
||||||
</div>
|
</Column>
|
||||||
</div>
|
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true">
|
||||||
<div class="separator separator-dashed my-3"></div>
|
<Template>
|
||||||
<div class="d-flex flex-stack">
|
<a href="/admin/nodes/view/@(context.Id)">@(context.Name)</a>
|
||||||
<div class="d-flex align-items-center me-3">
|
</Template>
|
||||||
<div class="flex-grow-1">
|
</Column>
|
||||||
<span class="text-gray-800 fs-5 fw-bold lh-0">
|
<Column TableItem="Node" Title="@(SmartTranslateService.Translate("Fqdn"))" Field="@(x => x.Fqdn)" Sortable="true" Filterable="true"/>
|
||||||
<TL>CPU Usage</TL>
|
<Column TableItem="Node" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||||
</span>
|
<Template>
|
||||||
<span class="text-gray-400 fw-semibold d-block fs-6">
|
<a href="/admin/nodes/edit/@(context.Id)">
|
||||||
<TL>In %</TL>
|
@(SmartTranslateService.Translate("Edit"))
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center w-100 mw-125px">
|
|
||||||
<span class="text-white-400 fw-semibold">
|
|
||||||
@{
|
|
||||||
var cpu = CpuCache.ContainsKey(node) ? CpuCache[node] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (cpu == null)
|
|
||||||
{
|
|
||||||
<span>Loading</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>@(cpu.CpuUsage)%</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="separator separator-dashed my-3"></div>
|
|
||||||
<div class="d-flex flex-stack">
|
|
||||||
<div class="d-flex align-items-center me-3">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<span class="text-gray-800 fs-5 fw-bold lh-0">
|
|
||||||
<TL>Memory</TL>
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-400 fw-semibold d-block fs-6">
|
|
||||||
<TL>Used / Available memory</TL>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center w-100 mw-125px">
|
|
||||||
<span class="text-white-400 fw-semibold">
|
|
||||||
@{
|
|
||||||
var memory = MemoryCache.ContainsKey(node) ? MemoryCache[node] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (memory == null)
|
|
||||||
{
|
|
||||||
<span>Loading</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>@(Formatter.FormatSize(memory.Total - memory.Free)) / @(Formatter.FormatSize(memory.Total))</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="separator separator-dashed my-3"></div>
|
|
||||||
<div class="d-flex flex-stack">
|
|
||||||
<div class="d-flex align-items-center me-3">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<span class="text-gray-800 fs-5 fw-bold lh-0">
|
|
||||||
<TL>Storage</TL>
|
|
||||||
</span>
|
|
||||||
<span class="text-gray-400 fw-semibold d-block fs-6">
|
|
||||||
<TL>Available storage</TL>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex align-items-center w-100 mw-125px">
|
|
||||||
<span class="text-white-400 fw-semibold">
|
|
||||||
@{
|
|
||||||
var disk = DiskCache.ContainsKey(node) ? DiskCache[node] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (disk == null)
|
|
||||||
{
|
|
||||||
<span>Loading</span>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<span>@(Formatter.FormatSize(disk.FreeBytes))</span>
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="separator my-5"></div>
|
|
||||||
<div class="d-flex flex-stack">
|
|
||||||
<div class="align-items-start">
|
|
||||||
<a class="btn btn-primary" href="/admin/nodes/edit/@(node.Id)">
|
|
||||||
<TL>Edit</TL>
|
|
||||||
</a>
|
</a>
|
||||||
<a class="btn btn-success" href="/admin/nodes/setup/@(node.Id)">
|
</Template>
|
||||||
<TL>Setup</TL>
|
</Column>
|
||||||
|
<Column TableItem="Node" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||||
|
<Template>
|
||||||
|
<a href="/admin/nodes/setup/@(context.Id)">
|
||||||
|
@(SmartTranslateService.Translate("Setup"))
|
||||||
</a>
|
</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
|
<Column TableItem="Node" Title="" Field="@(x => x.Id)" Sortable="false" Filterable="false">
|
||||||
|
<Template>
|
||||||
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
|
<WButton Text="@(SmartTranslateService.Translate("Delete"))"
|
||||||
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
|
WorkingText="@(SmartTranslateService.Translate("Deleting"))"
|
||||||
CssClasses="btn-danger"
|
CssClasses="btn-sm btn-danger"
|
||||||
OnClick="() => Delete(node)">
|
OnClick="() => Delete(context)">
|
||||||
</WButton>
|
</WButton>
|
||||||
</div>
|
</Template>
|
||||||
</div>
|
</Column>
|
||||||
</div>
|
<Pager ShowPageNumber="true" ShowTotalCount="true"/>
|
||||||
</div>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<TL>No nodes found</TL>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</LazyLoader>
|
</LazyLoader>
|
||||||
</div>
|
</div>
|
||||||
</OnlyAdmin>
|
</OnlyAdmin>
|
||||||
@@ -186,17 +102,10 @@
|
|||||||
|
|
||||||
private LazyLoader LazyLoader;
|
private LazyLoader LazyLoader;
|
||||||
|
|
||||||
private Dictionary<Node, CpuStats> CpuCache = new();
|
|
||||||
private Dictionary<Node, MemoryStats> MemoryCache = new();
|
|
||||||
private Dictionary<Node, DiskStats> DiskCache = new();
|
|
||||||
private Dictionary<Node, SystemStatus?> StatusCache = new();
|
private Dictionary<Node, SystemStatus?> StatusCache = new();
|
||||||
|
|
||||||
private Task Load(LazyLoader lazyLoader)
|
private Task Load(LazyLoader lazyLoader)
|
||||||
{
|
{
|
||||||
CpuCache.Clear();
|
|
||||||
MemoryCache.Clear();
|
|
||||||
DiskCache.Clear();
|
|
||||||
|
|
||||||
lock (StatusCache)
|
lock (StatusCache)
|
||||||
{
|
{
|
||||||
StatusCache.Clear();
|
StatusCache.Clear();
|
||||||
@@ -222,11 +131,6 @@
|
|||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Logger.Debug(e.Message);
|
Logger.Debug(e.Message);
|
||||||
|
|
||||||
lock (StatusCache)
|
|
||||||
{
|
|
||||||
StatusCache.Add(node, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|||||||
357
Moonlight/Shared/Views/Admin/Nodes/View.razor
Normal file
357
Moonlight/Shared/Views/Admin/Nodes/View.razor
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
@page "/admin/nodes/view/{id:int}"
|
||||||
|
|
||||||
|
@using Moonlight.App.Repositories
|
||||||
|
@using Moonlight.App.Database.Entities
|
||||||
|
@using Moonlight.App.Helpers
|
||||||
|
@using Moonlight.App.Models.Daemon.Resources
|
||||||
|
@using Moonlight.App.Models.Wings.Resources
|
||||||
|
@using Moonlight.App.Services
|
||||||
|
|
||||||
|
@inject NodeRepository NodeRepository
|
||||||
|
@inject NodeService NodeService
|
||||||
|
|
||||||
|
<OnlyAdmin>
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
@if (Node == null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<TL>No node with this id found</TL>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex flex-center">
|
||||||
|
<div class="row">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">
|
||||||
|
<span class="fw-bold fs-3">
|
||||||
|
@(Node.Name) <TL>details</TL>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 g-lg-6">
|
||||||
|
<div class="col">
|
||||||
|
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
|
||||||
|
<div class="symbol symbol-30px me-5 mb-8">
|
||||||
|
<span class="symbol-label">
|
||||||
|
<i class="text-primary bx bx-lg bx-chip"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-0">
|
||||||
|
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
|
||||||
|
@if (CpuStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
@(CpuStats.Usage)% <TL>of</TL> @(CpuStats.Cores) <TL>Cores used</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="fw-semibold fs-6">
|
||||||
|
@if (CpuStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>@(CpuStats.Model)</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
|
||||||
|
<div class="symbol symbol-30px me-5 mb-8">
|
||||||
|
<span class="symbol-label">
|
||||||
|
<i class="text-primary bx bx-lg bx-microchip"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-0">
|
||||||
|
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
|
||||||
|
@if (MemoryStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
@(Formatter.FormatSize(MemoryStats.Used * 1024D * 1024D)) <TL>of</TL> @(Formatter.FormatSize(MemoryStats.Total * 1024D * 1024D)) <TL>used</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="fw-semibold fs-6">
|
||||||
|
@if (MemoryStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (MemoryStats.Sticks.Any())
|
||||||
|
{
|
||||||
|
foreach (var stick in SortMemorySticks(MemoryStats.Sticks))
|
||||||
|
{
|
||||||
|
<span>@(stick)</span>
|
||||||
|
<br/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>No memory sticks detected</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
|
||||||
|
<div class="symbol symbol-30px me-5 mb-8">
|
||||||
|
<span class="symbol-label">
|
||||||
|
<i class="text-primary bx bx-lg bx-microchip"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-0">
|
||||||
|
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
|
||||||
|
@if (DiskStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
@(Formatter.FormatSize(DiskStats.TotalSize - DiskStats.FreeBytes)) <TL>of</TL> @(Formatter.FormatSize(DiskStats.TotalSize)) <TL>used</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="fw-semibold fs-6">
|
||||||
|
@if (DiskStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>@(DiskStats.Name) - @(DiskStats.DriveFormat)</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 row g-3 g-lg-6">
|
||||||
|
<div class="col">
|
||||||
|
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
|
||||||
|
<div class="symbol symbol-30px me-5 mb-8">
|
||||||
|
<span class="symbol-label">
|
||||||
|
<i class="text-primary bx bx-lg bx-purchase-tag"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-0">
|
||||||
|
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
|
||||||
|
@if (SystemStatus == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-success">
|
||||||
|
<TL>Online</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="fw-semibold fs-6">
|
||||||
|
@if (SystemStatus == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>@(SystemStatus.Version)</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
|
||||||
|
<div class="symbol symbol-30px me-5 mb-8">
|
||||||
|
<span class="symbol-label">
|
||||||
|
<i class="text-primary bx bx-lg bx-fingerprint"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-0">
|
||||||
|
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
|
||||||
|
@if (SystemStatus == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
@(SystemStatus.KernelVersion) - @(SystemStatus.Architecture)
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="fw-semibold fs-6">
|
||||||
|
@if (SystemStatus == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<TL>Host system information</TL>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<div class="bg-gray-100 bg-opacity-70 rounded-2 px-6 py-5">
|
||||||
|
<div class="symbol symbol-30px me-5 mb-8">
|
||||||
|
<span class="symbol-label">
|
||||||
|
<i class="text-primary bx bx-lg bxl-docker"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="m-0">
|
||||||
|
<span class="fw-bolder d-block fs-2qx lh-1 ls-n1 mb-1">
|
||||||
|
@if (ContainerStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>
|
||||||
|
<TL>@(ContainerStats.Containers.Count)</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="fw-semibold fs-6">
|
||||||
|
@if (ContainerStats == null)
|
||||||
|
{
|
||||||
|
<span class="text-muted">
|
||||||
|
<TL>Loading</TL>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<TL>Docker containers running</TL>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 card card-body">
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<a href="/admin/nodes" class="btn btn-primary">
|
||||||
|
<TL>Cancel</TL>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</LazyLoader>
|
||||||
|
</OnlyAdmin>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
[Parameter]
|
||||||
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
private Node? Node;
|
||||||
|
|
||||||
|
private CpuStats CpuStats;
|
||||||
|
private MemoryStats MemoryStats;
|
||||||
|
private DiskStats DiskStats;
|
||||||
|
private SystemStatus SystemStatus;
|
||||||
|
private ContainerStats ContainerStats;
|
||||||
|
|
||||||
|
private async Task Load(LazyLoader arg)
|
||||||
|
{
|
||||||
|
Node = NodeRepository
|
||||||
|
.Get()
|
||||||
|
.FirstOrDefault(x => x.Id == Id);
|
||||||
|
|
||||||
|
if (Node != null)
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SystemStatus = await NodeService.GetStatus(Node);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
CpuStats = await NodeService.GetCpuStats(Node);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
MemoryStats = await NodeService.GetMemoryStats(Node);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
DiskStats = await NodeService.GetDiskStats(Node);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
|
ContainerStats = await NodeService.GetContainerStats(Node);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<string> SortMemorySticks(List<MemoryStats.MemoryStick> sticks)
|
||||||
|
{
|
||||||
|
// Thank you ChatGPT <3
|
||||||
|
|
||||||
|
var groupedMemory = sticks.GroupBy(memory => new { memory.Type, memory.Size })
|
||||||
|
.Select(group => new
|
||||||
|
{
|
||||||
|
Type = group.Key.Type,
|
||||||
|
Size = group.Key.Size,
|
||||||
|
Count = group.Count()
|
||||||
|
});
|
||||||
|
|
||||||
|
var sortedMemory = groupedMemory.OrderBy(memory => memory.Type)
|
||||||
|
.ThenBy(memory => memory.Size);
|
||||||
|
|
||||||
|
List<string> sortedList = sortedMemory.Select(memory =>
|
||||||
|
{
|
||||||
|
string sizeString = $"{memory.Size}GB";
|
||||||
|
return $"{memory.Count}x {memory.Type} {sizeString}";
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
return sortedList;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,11 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<Table TableItem="Server" Items="Servers" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
<Table TableItem="Server" Items="Servers" PageSize="25" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3" TableHeadClass="fw-bold text-muted">
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Id"))" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Name"))" Field="@(x => x.Name)" Sortable="true" Filterable="true">
|
||||||
|
<Template>
|
||||||
|
<a href="/server/@(context.Uuid)">@(context.Name)</a>
|
||||||
|
</Template>
|
||||||
|
</Column>
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Cores"))" Field="@(x => x.Cpu)" Sortable="true" Filterable="true"/>
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Cores"))" Field="@(x => x.Cpu)" Sortable="true" Filterable="true"/>
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Memory"))" Field="@(x => x.Memory)" Sortable="true" Filterable="true"/>
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Memory"))" Field="@(x => x.Memory)" Sortable="true" Filterable="true"/>
|
||||||
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Disk"))" Field="@(x => x.Disk)" Sortable="true" Filterable="true"/>
|
<Column TableItem="Server" Title="@(SmartTranslateService.Translate("Disk"))" Field="@(x => x.Disk)" Sortable="true" Filterable="true"/>
|
||||||
|
|||||||
@@ -354,3 +354,15 @@ Allocations;Allocations
|
|||||||
No variables found;No variables found
|
No variables found;No variables found
|
||||||
Successfully added image;Successfully added image
|
Successfully added image;Successfully added image
|
||||||
Password change for;Password change for
|
Password change for;Password change for
|
||||||
|
of;of
|
||||||
|
New node;New node
|
||||||
|
Fqdn;Fqdn
|
||||||
|
Cores used;Cores used
|
||||||
|
used;used
|
||||||
|
5.15.90.1-microsoft-standard-WSL2 - amd64;5.15.90.1-microsoft-standard-WSL2 - amd64
|
||||||
|
Host system information;Host system information
|
||||||
|
0;0
|
||||||
|
Docker containers running;Docker containers running
|
||||||
|
details;details
|
||||||
|
1;1
|
||||||
|
2;2
|
||||||
|
|||||||
Reference in New Issue
Block a user