New v2 project structure
This commit is contained in:
219
Moonlight/Features/Servers/UI/UserViews/Backups.razor
Normal file
219
Moonlight/Features/Servers/UI/UserViews/Backups.razor
Normal file
@@ -0,0 +1,219 @@
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using MoonCore.Abstractions
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using MoonCore.Helpers
|
||||
@using MoonCoreUI.Services
|
||||
@using Moonlight.Features.Servers.Events
|
||||
@using Moonlight.Features.Servers.Helpers
|
||||
@using Moonlight.Features.Servers.Models.Enums
|
||||
@using Moonlight.Features.Servers.Services
|
||||
|
||||
@inject IServiceProvider ServiceProvider
|
||||
@inject ServerService ServerService
|
||||
@inject ToastService ToastService
|
||||
@inject AlertService AlertService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<div class="card card-body px-5 py-3 mb-5">
|
||||
<div class="d-flex justify-content-end">
|
||||
<WButton OnClick="Create" CssClasses="btn btn-primary" Text="Create backup"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (AllBackups.Any())
|
||||
{
|
||||
foreach (var backup in AllBackups)
|
||||
{
|
||||
<div class="card card-body px-5 py-2 my-3">
|
||||
<div class="row">
|
||||
<div class="col-2 fs-5 d-flex align-items-center">
|
||||
<div class="symbol symbol-circle bg-secondary text-center d-flex justify-content-center align-items-center">
|
||||
@if (backup.Completed)
|
||||
{
|
||||
if (backup.Successful)
|
||||
{
|
||||
<i class="bx bx-sm bx-check-circle text-success align-middle p-2"></i>
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bx bx-sm bx-error-circle text-danger align-middle p-2"></i>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<i class="bx bx-sm bx-time-five text-white align-middle p-2"></i>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 col-5 fs-5 d-flex align-items-center justify-content-center">
|
||||
@if (backup.Completed)
|
||||
{
|
||||
if (backup.Successful)
|
||||
{
|
||||
<span><span class="d-none d-md-inline">Created at </span>@(Formatter.FormatDate(backup.CreatedAt))</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span><span class="d-none d-md-inline">Failed at </span>@(Formatter.FormatDate(backup.CreatedAt))</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Creating...</span>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3 d-none d-md-flex fs-5 d-flex align-items-center justify-content-center">
|
||||
@(Formatter.FormatSize(backup.Size))
|
||||
</div>
|
||||
<div class="col-md-3 col-5 d-flex justify-content-end">
|
||||
@if (backup.Completed)
|
||||
{
|
||||
if (backup.Successful)
|
||||
{
|
||||
if (Console.State == ServerState.Offline)
|
||||
{
|
||||
<WButton OnClick="() => Restore(backup)" CssClasses="btn btn-icon btn-primary me-2">
|
||||
<i class="bx bx-sm bx-revision"></i>
|
||||
</WButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-icon btn-primary disabled me-2" disabled="">
|
||||
<i class="bx bx-sm bx-revision"></i>
|
||||
</button>
|
||||
}
|
||||
<WButton OnClick="() => Download(backup)" CssClasses="btn btn-icon btn-info me-2">
|
||||
<i class="bx bx-sm bx-download"></i>
|
||||
</WButton>
|
||||
<WButton OnClick="() => Delete(backup)" CssClasses="btn btn-icon btn-danger">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</WButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-icon btn-primary disabled me-2" disabled="">
|
||||
<i class="bx bx-sm bx-revision"></i>
|
||||
</button>
|
||||
<button class="btn btn-icon btn-info disabled me-2" disabled="">
|
||||
<i class="bx bx-sm bx-download"></i>
|
||||
</button>
|
||||
<WButton OnClick="() => Delete(backup, false)" CssClasses="btn btn-icon btn-danger">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</WButton>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-icon btn-primary disabled me-2" disabled="">
|
||||
<i class="bx bx-sm bx-revision"></i>
|
||||
</button>
|
||||
<button class="btn btn-icon btn-info disabled me-2" disabled="">
|
||||
<i class="bx bx-sm bx-download"></i>
|
||||
</button>
|
||||
<button class="btn btn-icon btn-danger disabled" disabled="">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<IconAlert Title="No backups found" Color="primary" Icon="bx-search-alt">
|
||||
We were unable to find any backups associated to this server. Create a backup to start securing your data
|
||||
</IconAlert>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
[CascadingParameter] public ServerConsole Console { get; set; }
|
||||
|
||||
private ServerBackup[] AllBackups;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
ServerEvents.OnBackupCompleted += HandleBackupCompleted;
|
||||
Console.OnStateChange += HandleStateChange;
|
||||
}
|
||||
|
||||
private Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
// We need to use a new scope here in order ty bypass the cache of the repo (which comes from ef core)
|
||||
using var scope = ServiceProvider.CreateScope();
|
||||
var serverRepo = scope.ServiceProvider.GetRequiredService<Repository<Server>>();
|
||||
|
||||
AllBackups = serverRepo
|
||||
.Get()
|
||||
.Include(x => x.Backups)
|
||||
.First(x => x.Id == Server.Id)
|
||||
.Backups
|
||||
.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task Create()
|
||||
{
|
||||
await ServerService.Backup.Create(Server);
|
||||
|
||||
await ToastService.Info("Started backup creation");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
public async Task Restore(ServerBackup backup)
|
||||
{
|
||||
if(!await AlertService.YesNo("Do you really want to restore this backup? All files on the server will be deleted and replaced by the backup"))
|
||||
return;
|
||||
|
||||
await ServerService.Backup.Restore(Server, backup);
|
||||
|
||||
await ToastService.Success("Successfully restored backup");
|
||||
}
|
||||
|
||||
public async Task Delete(ServerBackup backup, bool safeDelete = true)
|
||||
{
|
||||
if(!await AlertService.YesNo("Do you really want to delete this backup? Deleted backups cannot be restored"))
|
||||
return;
|
||||
|
||||
await ServerService.Backup.Delete(Server, backup, safeDelete);
|
||||
|
||||
await ToastService.Success("Successfully deleted backup");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task Download(ServerBackup backup)
|
||||
{
|
||||
var url = await ServerService.Backup.GetDownloadUrl(Server, backup);
|
||||
Navigation.NavigateTo(url, true);
|
||||
}
|
||||
|
||||
private async Task HandleBackupCompleted((Server server, ServerBackup backup) data)
|
||||
{
|
||||
if (data.server.Id != Server.Id)
|
||||
return;
|
||||
|
||||
if (data.backup.Successful)
|
||||
await ToastService.Success("Successfully created backup");
|
||||
else
|
||||
await ToastService.Danger("Backup creation failed");
|
||||
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task HandleStateChange(ServerState _) => await InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ServerEvents.OnBackupCompleted -= HandleBackupCompleted;
|
||||
Console.OnStateChange -= HandleStateChange;
|
||||
}
|
||||
}
|
||||
128
Moonlight/Features/Servers/UI/UserViews/Console.razor
Normal file
128
Moonlight/Features/Servers/UI/UserViews/Console.razor
Normal file
@@ -0,0 +1,128 @@
|
||||
@using Moonlight.Features.Servers.Models.Abstractions
|
||||
@using Moonlight.Features.Servers.Services
|
||||
@using Moonlight.Features.Servers.UI.Components
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using Moonlight.Features.Servers.Helpers
|
||||
@using Moonlight.Features.Servers.Api.Packets
|
||||
@using Moonlight.Features.Servers.Models.Enums
|
||||
@using MoonCore.Helpers
|
||||
@using ApexCharts
|
||||
|
||||
@inject ServerService ServerService
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-md-9 col-12">
|
||||
<div class="card card-body bg-black border-0 p-3 h-100">
|
||||
<Terminal @ref="Terminal"/>
|
||||
<div class="mt-3">
|
||||
<div class="input-group">
|
||||
<input @bind="CommandInput" class="form-control form-control-transparent text-white" placeholder="Enter command"/>
|
||||
<WButton CssClasses="btn btn-secondary rounded-start" Text="Execute" OnClick="SendCommand"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="pb-3">
|
||||
@{
|
||||
var coreText = Server.Cpu > 100 ? "Cores" : "Core";
|
||||
var cpuText = $"{Math.Round(CurrentStats.CpuUsage / Server.Cpu * 100, 1)}% / {Math.Round(Server.Cpu / 100D, 2)} {coreText}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-chip" Description="CPU Usage" Value="@cpuText"/>
|
||||
</div>
|
||||
|
||||
<div class="pb-3">
|
||||
@{
|
||||
string memoryHas;
|
||||
|
||||
if (Server.Memory >= 1024)
|
||||
memoryHas = $"{ByteSizeValue.FromMegaBytes(Server.Memory).GigaBytes} GB";
|
||||
else
|
||||
memoryHas = $"{Server.Memory} MB";
|
||||
|
||||
var memoryText = $"{Formatter.FormatSize(CurrentStats.MemoryUsage)} / {memoryHas}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-microchip" Description="Memory Usage" Value="@memoryText"/>
|
||||
</div>
|
||||
|
||||
<div class="pb-3">
|
||||
@{
|
||||
var networkText = $"{Formatter.FormatSize(CurrentStats.NetRead)} / {Formatter.FormatSize(CurrentStats.NetWrite)}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-transfer-alt" Description="Network Traffic (Read / Write)" Value="@networkText"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public ServerConsole ServerConsole { get; set; }
|
||||
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
private Terminal Terminal;
|
||||
private string CommandInput = "";
|
||||
|
||||
private ServerStats CurrentStats = new();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
var text = "";
|
||||
|
||||
foreach (var line in ServerConsole.Messages.TakeLast(50))
|
||||
text += line + "\n\r";
|
||||
|
||||
await Terminal.Write(text);
|
||||
|
||||
|
||||
ServerConsole.OnNewMessage += OnMessage;
|
||||
|
||||
ServerConsole.OnStatsChange += HandleStats;
|
||||
ServerConsole.OnStateChange += HandleState;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleState(ServerState state)
|
||||
{
|
||||
if (state == ServerState.Offline || state == ServerState.Join2Start)
|
||||
{
|
||||
CurrentStats = new();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleStats(ServerStats stats)
|
||||
{
|
||||
CurrentStats = stats;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnMessage(string message)
|
||||
{
|
||||
await Terminal.WriteLine(message);
|
||||
}
|
||||
|
||||
private async Task SendCommand()
|
||||
{
|
||||
await ServerService.Console.SendCommand(Server, CommandInput);
|
||||
CommandInput = "";
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ServerConsole.OnStateChange -= HandleState;
|
||||
ServerConsole.OnStatsChange -= HandleStats;
|
||||
ServerConsole.OnNewMessage -= OnMessage;
|
||||
}
|
||||
}
|
||||
33
Moonlight/Features/Servers/UI/UserViews/Files.razor
Normal file
33
Moonlight/Features/Servers/UI/UserViews/Files.razor
Normal file
@@ -0,0 +1,33 @@
|
||||
@using Moonlight.Core.Configuration
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using MoonCore.Services
|
||||
@using Moonlight.Core.Services
|
||||
@using Moonlight.Features.FileManager.Models.Abstractions.FileAccess
|
||||
@using Moonlight.Features.FileManager.UI.Components
|
||||
@using Moonlight.Features.Servers.Helpers
|
||||
@using Moonlight.Features.Servers.Services
|
||||
|
||||
@inject ServerService ServerService
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<LazyLoader Load="Load" ShowAsCard="true">
|
||||
<FileManager FileAccess="FileAccess" />
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
private IFileAccess FileAccess;
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
FileAccess = await ServerService.OpenFileAccess(Server);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FileAccess.Dispose();
|
||||
}
|
||||
}
|
||||
218
Moonlight/Features/Servers/UI/UserViews/Network.razor
Normal file
218
Moonlight/Features/Servers/UI/UserViews/Network.razor
Normal file
@@ -0,0 +1,218 @@
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using System.Net
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using MoonCore.Abstractions
|
||||
@using MoonCore.Helpers
|
||||
@using MoonCoreUI.Services
|
||||
|
||||
@inject Repository<ServerNetwork> NetworkRepository
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject ToastService ToastService
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-5">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="fs-4 fw-semibold">
|
||||
<i class="bx bx-md bx-globe text-info me-3 align-middle"></i> Public Network
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
@if (!Server.DisablePublicNetwork)
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" checked="checked" @onchange="() => UpdatePublicNetwork(true)">
|
||||
}
|
||||
else
|
||||
{
|
||||
<input class="form-check-input" type="checkbox" @onchange="() => UpdatePublicNetwork(false)">
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card card-body mb-15 py-3 px-5">
|
||||
@if (!Server.DisablePublicNetwork)
|
||||
{
|
||||
<CrudTable TItem="ServerAllocation" Items="Server.Allocations" PageSize="25" ShowPagination="false">
|
||||
<CrudColumn TItem="ServerAllocation" Field="@(x => x.IpAddress)" Title="FQDN or dedicated IP">
|
||||
<Template>
|
||||
@if (context!.IpAddress == "0.0.0.0")
|
||||
{
|
||||
if (IsIpAddress(Server.Node.Fqdn))
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@Server.Node.Fqdn</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@context.IpAddress</span>
|
||||
}
|
||||
</Template>
|
||||
</CrudColumn>
|
||||
<CrudColumn TItem="ServerAllocation" Field="@(x => x.IpAddress)" Title="IP address">
|
||||
<Template>
|
||||
@if (context!.IpAddress == "0.0.0.0")
|
||||
{
|
||||
if (IsIpAddress(Server.Node.Fqdn))
|
||||
{
|
||||
<span>@Server.Node.Fqdn</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>1.2.3.4</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</Template>
|
||||
</CrudColumn>
|
||||
<CrudColumn TItem="ServerAllocation" Field="@(x => x.Port)" Title="Port"/>
|
||||
<CrudColumn TItem="ServerAllocation" Field="@(x => x.Note)" Title="Notes">
|
||||
<Template>
|
||||
<input class="form-control" placeholder="What is this allocation for?"/>
|
||||
</Template>
|
||||
</CrudColumn>
|
||||
<CrudColumn TItem="ServerAllocation">
|
||||
<Template>
|
||||
<div class="d-flex justify-content-end">
|
||||
<WButton CssClasses="btn btn-icon btn-danger disabled">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</WButton>
|
||||
</div>
|
||||
</Template>
|
||||
</CrudColumn>
|
||||
</CrudTable>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-center fs-4">Public network is disabled</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card mb-5">
|
||||
<div class="card-body p-5">
|
||||
<div class="fs-4 fw-semibold">
|
||||
<i class="bx bx-md bx-lock-alt text-info me-3 align-middle"></i> Private Network
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (Networks.Length == 0)
|
||||
{
|
||||
<div class="card card-body py-3 px-5">
|
||||
<IconAlert Icon="bx-search-alt" Color="primary" Title="No private network found">
|
||||
Create a new private network in order to connect multiple servers on the same node
|
||||
<div class="d-flex justify-content-center mt-5 mb-5">
|
||||
<a href="/servers/networks" class="btn btn-primary">Create private network</a>
|
||||
</div>
|
||||
</IconAlert>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Server.Network == null)
|
||||
{
|
||||
<div class="row g-5">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body">
|
||||
<IconAlert Color="info" Icon="bx-id-card" Title="Network Identity">
|
||||
<div>Visible to other servers when in a private network as:</div>
|
||||
<div class="text-primary fw-semibold fs-3 my-3">moonlight-runtime-@(Server.Id)</div>
|
||||
</IconAlert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
@foreach (var network in Networks)
|
||||
{
|
||||
<div class="card card-body px-5 mb-3">
|
||||
<div class="row">
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
<div class="fs-4 fw-semibold">@(network.Name)</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex justify-content-end">
|
||||
<WButton CssClasses="btn btn-primary" Text="Join network" OnClick="() => SetNetwork(network)"></WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="row g-5">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body">
|
||||
<IconAlert Color="info" Icon="bx-id-card" Title="Network Identity">
|
||||
<div>Visible to other servers as:</div>
|
||||
<div class="text-primary fw-semibold fs-3 my-3">moonlight-runtime-@(Server.Id)</div>
|
||||
</IconAlert>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="d-flex flex-row">
|
||||
<div class="card card-body px-5">
|
||||
<div class="row">
|
||||
<div class="col-6 d-flex align-items-center">
|
||||
<div class="fs-4 fw-semibold">@(Server.Network.Name)</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="d-flex justify-content-end">
|
||||
<WButton CssClasses="btn btn-danger" Text="Leave network" OnClick="() => SetNetwork(null)"></WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
private bool IsIpAddress(string input) => IPAddress.TryParse(input, out _);
|
||||
|
||||
private ServerNetwork[] Networks;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
await lazyLoader.SetText("Loading available networks");
|
||||
|
||||
Networks = NetworkRepository
|
||||
.Get()
|
||||
.Where(x => x.User.Id == Server.Owner.Id)
|
||||
.Where(x => x.Node.Id == Server.Node.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private async Task SetNetwork(ServerNetwork? network)
|
||||
{
|
||||
Server.Network = network;
|
||||
ServerRepository.Update(Server);
|
||||
|
||||
await ToastService.Success("Successfully updated network state");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task UpdatePublicNetwork(bool value)
|
||||
{
|
||||
Server.DisablePublicNetwork = value;
|
||||
ServerRepository.Update(Server);
|
||||
|
||||
await ToastService.Success("Successfully updated public network state");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
133
Moonlight/Features/Servers/UI/UserViews/Reset.razor
Normal file
133
Moonlight/Features/Servers/UI/UserViews/Reset.razor
Normal file
@@ -0,0 +1,133 @@
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using Moonlight.Features.Servers.Helpers
|
||||
@using Moonlight.Features.Servers.Models.Abstractions
|
||||
@using Moonlight.Features.Servers.Models.Enums
|
||||
@using Moonlight.Features.Servers.Services
|
||||
@using MoonCoreUI.Services
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
@inject ServerService ServerService
|
||||
@inject AlertService AlertService
|
||||
@inject ToastService ToastService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
<div class="row g-8">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-8 h-100">
|
||||
<p class="fs-6">
|
||||
This will run the install script of the image again. Server files will be changed or deleted so be cautious
|
||||
</p>
|
||||
@if (Console.State == ServerState.Offline)
|
||||
{
|
||||
<WButton OnClick="Reinstall" CssClasses="btn btn-primary mt-auto" Text="Reinstall"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-primary disabled mt-auto" disabled="">Reinstall</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-8 h-100">
|
||||
<p class="fs-6">
|
||||
This will delete all files and run the install script. Please make sure you create a backup before resetting the server
|
||||
</p>
|
||||
@if (Console.State == ServerState.Offline)
|
||||
{
|
||||
<WButton OnClick="ResetServer" CssClasses="btn btn-warning mt-auto" Text="Reset"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-warning disabled mt-auto" disabled="">Reset</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12"> @* TODO: Make deleting configurable to show or not *@
|
||||
<div class="card card-body p-8 h-100">
|
||||
<p class="fs-6">
|
||||
This deletes your server. The deleted data is not recoverable. Please make sure you have a backup of the data before deleting the server
|
||||
</p>
|
||||
@if (Console.State == ServerState.Offline)
|
||||
{
|
||||
<WButton OnClick="Delete" CssClasses="btn btn-danger mt-auto" Text="Delete"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button class="btn btn-danger disabled mt-auto" disabled="">Delete</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
[CascadingParameter] public ServerConsole Console { get; set; }
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
Console.OnStateChange += OnStateChanged;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Reinstall()
|
||||
{
|
||||
if (!await AlertService.YesNo("Do you want to reinstall this server? This may replace/delete some files"))
|
||||
return;
|
||||
|
||||
await ServerService.Console.SendAction(Server, PowerAction.Install);
|
||||
}
|
||||
|
||||
private async Task ResetServer()
|
||||
{
|
||||
if (!await AlertService.YesNo("Do you want to reset this server? This will delete all files and run the install script"))
|
||||
return;
|
||||
|
||||
await ToastService.CreateProgress("serverReset", "Reset: Deleting files");
|
||||
|
||||
using var fileAccess = await ServerService.OpenFileAccess(Server);
|
||||
|
||||
var files = await fileAccess.List();
|
||||
int i = 0;
|
||||
|
||||
foreach (var fileEntry in files)
|
||||
{
|
||||
i++;
|
||||
|
||||
await ToastService.ModifyProgress("serverReset", $"Reset: Deleting files [{i} / {files.Length}]");
|
||||
await fileAccess.Delete(fileEntry.Name);
|
||||
}
|
||||
|
||||
await ToastService.ModifyProgress("serverReset", "Reset: Starting install script");
|
||||
|
||||
await ServerService.Console.SendAction(Server, PowerAction.Install);
|
||||
|
||||
await ToastService.RemoveProgress("serverReset");
|
||||
}
|
||||
|
||||
private async Task Delete()
|
||||
{
|
||||
var input = await AlertService.Text($"Please type '{Server.Name}' to confirm deleting this server");
|
||||
|
||||
if(input != Server.Name)
|
||||
return;
|
||||
|
||||
await ServerService.Delete(Server);
|
||||
|
||||
await ToastService.Success("Successfully deleted server");
|
||||
Navigation.NavigateTo("/servers");
|
||||
}
|
||||
|
||||
private async Task OnStateChanged(ServerState _) => await InvokeAsync(StateHasChanged);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Console.OnStateChange -= OnStateChanged;
|
||||
}
|
||||
}
|
||||
390
Moonlight/Features/Servers/UI/UserViews/Schedules.razor
Normal file
390
Moonlight/Features/Servers/UI/UserViews/Schedules.razor
Normal file
@@ -0,0 +1,390 @@
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using MoonCore.Abstractions
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using MoonCore.Helpers
|
||||
@using MoonCoreUI.Services
|
||||
@using Moonlight.Features.Servers.Models.Forms.Users.Schedules
|
||||
@using Moonlight.Features.Servers.Services
|
||||
@using Newtonsoft.Json
|
||||
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject Repository<ServerSchedule> ScheduleRepository
|
||||
@inject Repository<ServerScheduleItem> ScheduleItemRepository
|
||||
@inject ServerScheduleService ScheduleService
|
||||
@inject ToastService ToastService
|
||||
@inject AlertService AlertService
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<div class="row g-5">
|
||||
<div class="col-md-3 col-12">
|
||||
<div class="card card-body p-5">
|
||||
<div class="d-flex flex-column">
|
||||
<WButton OnClick="CreateSchedule" CssClasses="btn btn-primary my-3 mb-5" Text="Create new schedule"/>
|
||||
|
||||
@foreach (var schedule in ServerWithSchedules.Schedules)
|
||||
{
|
||||
<WButton OnClick="() => SelectSchedule(schedule)" CssClasses="btn btn-secondary my-3" Text="@schedule.Name"/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-9 col-12">
|
||||
@if (SelectedSchedule == null)
|
||||
{
|
||||
<IconAlert Title="No schedule selected" Color="primary" Icon="bx-search-alt">
|
||||
Select or create a schedule in order to manage the it
|
||||
</IconAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card card-body px-4 pt-3 pb-3 mb-3 bg-secondary">
|
||||
<div class="row g-3 text-center">
|
||||
<div class="col-md-3 col-6 fs-6 d-flex align-items-center">
|
||||
<span>
|
||||
<span class="fw-bold me-2">Name:</span>@(SelectedSchedule.Name)
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-3 col-6 fs-6 d-flex align-items-center">
|
||||
<span>
|
||||
<span class="fw-bold me-2">Trigger</span>Every hour
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-2 col-6 fs-6 d-flex align-items-center">
|
||||
<span>
|
||||
<span class="fw-bold me-2">Last run:</span>@(SelectedSchedule.LastRun == DateTime.MinValue ? "Never" : Formatter.FormatDate(SelectedSchedule.LastRun))
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-2 col-5 fs-6 d-flex align-items-center">
|
||||
<span>
|
||||
<span class="fw-bold me-2">Execution time:</span>@(SelectedSchedule.ExecutionSeconds)s
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-2 col-12 d-flex justify-content-center justify-content-md-end">
|
||||
<WButton OnClick="RunSelectedSchedule" CssClasses="btn btn-icon btn-success me-2">
|
||||
<i class="bx bx-sm bx-play"></i>
|
||||
</WButton>
|
||||
<WButton OnClick="DeleteCurrentSchedule" CssClasses="btn btn-icon btn-danger">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column">
|
||||
@if (SelectedSchedule.Items.Any())
|
||||
{
|
||||
foreach (var item in SortedItems)
|
||||
{
|
||||
var action = ScheduleService.Actions.ContainsKey(item.Action) ? ScheduleService.Actions[item.Action] : null;
|
||||
|
||||
<div class="card card-body px-5 py-4 py-md-2 my-2">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-1 fs-5 d-none d-md-flex align-items-center">
|
||||
<div class="symbol symbol-circle bg-secondary text-center d-flex justify-content-center align-items-center">
|
||||
<i class="bx bx-sm @(action != null ? action.Icon : "") text-white align-middle p-3"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-8 col-12 fs-5 d-flex align-items-center justify-content-center justify-content-md-start">
|
||||
@if (action == null)
|
||||
{
|
||||
<div>Unknown action</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div>@action.DisplayName</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-md-3 col-12 d-flex justify-content-center justify-content-md-end">
|
||||
@if (action != null && action.FormType != typeof(object)) // Handle empty forms
|
||||
{
|
||||
<WButton OnClick="() => ConfigureScheduleItem(item)" CssClasses="btn btn-icon btn-primary me-2">
|
||||
<i class="bx bx-sm bx-cog"></i>
|
||||
</WButton>
|
||||
}
|
||||
@if (item.Priority == 0)
|
||||
{
|
||||
<button class="btn btn-icon btn-secondary me-2 disabled" disabled="">
|
||||
<i class="bx bx-sm bx-arrow-to-top"></i>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<WButton OnClick="() => MoveItem(item, -1)" CssClasses="btn btn-icon btn-secondary me-2">
|
||||
<i class="bx bx-sm bx-arrow-to-top"></i>
|
||||
</WButton>
|
||||
}
|
||||
@if (item == SortedItems.Last())
|
||||
{
|
||||
<button class="btn btn-icon btn-secondary me-2 disabled" disabled="">
|
||||
<i class="bx bx-sm bx-arrow-to-top"></i>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<WButton OnClick="() => MoveItem(item, 1)" CssClasses="btn btn-icon btn-secondary me-2">
|
||||
<i class="bx bx-sm bx-arrow-to-bottom"></i>
|
||||
</WButton>
|
||||
}
|
||||
<WButton OnClick="() => DeleteItem(item)" CssClasses="btn btn-icon btn-danger me-2">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mb-5">
|
||||
<IconAlert Title="No actions found" Color="primary" Icon="bx-search-alt">
|
||||
Create a new action in order to start automating your server
|
||||
</IconAlert>
|
||||
</div>
|
||||
}
|
||||
<div class="mt-5 d-flex justify-content-center">
|
||||
<div class="input-group">
|
||||
<select class="form-select" @bind="NewItemActionType">
|
||||
@foreach (var action in ScheduleService.Actions)
|
||||
{
|
||||
<option value="@action.Key">@action.Value.DisplayName</option>
|
||||
}
|
||||
</select>
|
||||
<WButton OnClick="CreateScheduleItem" CssClasses="btn btn-primary" Text="Add new action"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
<FormModalLauncher @ref="Launcher"/>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
private Server ServerWithSchedules;
|
||||
private LazyLoader LazyLoader;
|
||||
private FormModalLauncher Launcher;
|
||||
|
||||
private ServerSchedule? SelectedSchedule;
|
||||
private List<ServerScheduleItem> SortedItems = new();
|
||||
|
||||
private string NewItemActionType = "";
|
||||
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
await lazyLoader.SetText("Loading server schedules");
|
||||
|
||||
ServerWithSchedules = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Schedules)
|
||||
.ThenInclude(x => x.Items)
|
||||
.First(x => x.Id == Server.Id);
|
||||
|
||||
// Trigger reselect to update sort cache
|
||||
if (SelectedSchedule != null)
|
||||
await SelectSchedule(SelectedSchedule);
|
||||
}
|
||||
|
||||
private async Task SelectSchedule(ServerSchedule? schedule)
|
||||
{
|
||||
SelectedSchedule = schedule;
|
||||
|
||||
if (SelectedSchedule != null)
|
||||
{
|
||||
SortedItems = SelectedSchedule
|
||||
.Items
|
||||
.OrderBy(x => x.Priority)
|
||||
.ToList();
|
||||
}
|
||||
else
|
||||
SortedItems = new();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task CreateSchedule()
|
||||
{
|
||||
await Launcher.Show<CreateScheduleForm>("Create a new schedule", async form =>
|
||||
{
|
||||
ServerWithSchedules.Schedules.Add(new()
|
||||
{
|
||||
Name = form.Name
|
||||
});
|
||||
|
||||
ServerRepository.Update(ServerWithSchedules);
|
||||
|
||||
NewItemActionType = "";
|
||||
|
||||
await ToastService.Success("Successfully added new schedule");
|
||||
await LazyLoader.Reload();
|
||||
});
|
||||
}
|
||||
|
||||
private async Task CreateScheduleItem()
|
||||
{
|
||||
if (string.IsNullOrEmpty(NewItemActionType))
|
||||
return;
|
||||
|
||||
if (ScheduleService.Actions.All(x => x.Key != NewItemActionType))
|
||||
return;
|
||||
|
||||
if (SelectedSchedule == null)
|
||||
return;
|
||||
|
||||
var action = ScheduleService.Actions.First(x => x.Key == NewItemActionType).Value;
|
||||
|
||||
if (action.FormType == typeof(object)) // Handle empty form types
|
||||
{
|
||||
await AddScheduleAction(NewItemActionType, SelectedSchedule.Items.Count, new());
|
||||
return;
|
||||
}
|
||||
|
||||
await Launcher.Show("Configure action", async formData => { await AddScheduleAction(NewItemActionType, SelectedSchedule.Items.Count, formData); }, action.FormType);
|
||||
}
|
||||
|
||||
private async Task AddScheduleAction(string type, int priority, object data)
|
||||
{
|
||||
var scheduleItem = new ServerScheduleItem()
|
||||
{
|
||||
Action = type,
|
||||
Priority = priority,
|
||||
DataJson = JsonConvert.SerializeObject(data)
|
||||
};
|
||||
|
||||
SelectedSchedule!.Items.Add(scheduleItem);
|
||||
|
||||
ScheduleRepository.Update(SelectedSchedule);
|
||||
|
||||
NewItemActionType = ""; // Reset
|
||||
|
||||
await ToastService.Success("Successfully added new action");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task ConfigureScheduleItem(ServerScheduleItem item)
|
||||
{
|
||||
if (ScheduleService.Actions.All(x => x.Key != item.Action))
|
||||
return;
|
||||
|
||||
if (SelectedSchedule == null)
|
||||
return;
|
||||
|
||||
var action = ScheduleService.Actions.First(x => x.Key == item.Action).Value;
|
||||
|
||||
var formModel = JsonConvert.DeserializeObject(item.DataJson, action.FormType)!;
|
||||
|
||||
await Launcher.Show("Configure action", async formData =>
|
||||
{
|
||||
item.DataJson = JsonConvert.SerializeObject(formData);
|
||||
|
||||
ScheduleItemRepository.Update(item);
|
||||
|
||||
await ToastService.Success("Successfully updated action");
|
||||
}, action.FormType, formModel: formModel);
|
||||
}
|
||||
|
||||
private async Task MoveItem(ServerScheduleItem item, int move)
|
||||
{
|
||||
var oldIndex = SortedItems.IndexOf(item);
|
||||
|
||||
if (oldIndex == 0 && move < 0)
|
||||
return;
|
||||
|
||||
if (oldIndex == SortedItems.Count - 1 && move > 0)
|
||||
return;
|
||||
|
||||
SortedItems.RemoveAt(oldIndex);
|
||||
SortedItems.Insert(oldIndex + move, item);
|
||||
|
||||
await FixPriorities();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await ToastService.Success("Successfully moved item");
|
||||
}
|
||||
|
||||
private async Task DeleteItem(ServerScheduleItem item)
|
||||
{
|
||||
if (SelectedSchedule == null)
|
||||
return;
|
||||
|
||||
if (!await AlertService.YesNo("Do you really want to delete this action? This cannot be undone"))
|
||||
return;
|
||||
|
||||
SortedItems.Remove(item);
|
||||
SelectedSchedule.Items.Remove(item);
|
||||
|
||||
ScheduleRepository.Update(SelectedSchedule);
|
||||
|
||||
await FixPriorities();
|
||||
|
||||
await ToastService.Success("Successfully deleted action");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private Task FixPriorities()
|
||||
{
|
||||
// Fix priorities
|
||||
var i = 0;
|
||||
foreach (var scheduleItem in SortedItems)
|
||||
{
|
||||
scheduleItem.Priority = i;
|
||||
ScheduleItemRepository.Update(scheduleItem);
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DeleteCurrentSchedule()
|
||||
{
|
||||
if (SelectedSchedule == null)
|
||||
return;
|
||||
|
||||
if (!await AlertService.YesNo($"Do you really want to delete the schedule '{SelectedSchedule.Name}'? This cannot be undone"))
|
||||
return;
|
||||
|
||||
foreach (var item in SelectedSchedule.Items.ToArray())
|
||||
{
|
||||
try
|
||||
{
|
||||
ScheduleItemRepository.Delete(item);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
/* this should not fail the operation */
|
||||
}
|
||||
}
|
||||
|
||||
ScheduleRepository.Delete(SelectedSchedule);
|
||||
|
||||
SelectedSchedule = null;
|
||||
|
||||
await ToastService.Success("Successfully deleted schedule");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task RunSelectedSchedule()
|
||||
{
|
||||
if (SelectedSchedule == null)
|
||||
return;
|
||||
|
||||
await ToastService.CreateProgress("scheduleRun", "Running schedule");
|
||||
|
||||
var result = await ScheduleService.Run(Server, SelectedSchedule);
|
||||
|
||||
await ToastService.RemoveProgress("scheduleRun");
|
||||
|
||||
if (result.Failed)
|
||||
await ToastService.Danger($"Schedule run failed ({result.ExecutionSeconds}s)");
|
||||
else
|
||||
await ToastService.Success($"Schedule run was successful ({result.ExecutionSeconds}s)");
|
||||
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
122
Moonlight/Features/Servers/UI/UserViews/Stats.razor
Normal file
122
Moonlight/Features/Servers/UI/UserViews/Stats.razor
Normal file
@@ -0,0 +1,122 @@
|
||||
@using Moonlight.Features.Servers.Helpers
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using Moonlight.Features.Servers.Models.Abstractions
|
||||
@using Newtonsoft.Json
|
||||
@using ApexCharts
|
||||
@using MoonCore.Helpers
|
||||
@using Moonlight.Features.Servers.Api.Packets
|
||||
@using Moonlight.Features.Servers.Models.Enums
|
||||
@using Moonlight.Features.Servers.UI.Components
|
||||
|
||||
@implements IDisposable
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
<div class="row g-8">
|
||||
<div class="col-md-3 col-12">
|
||||
@{
|
||||
var coreText = Server.Cpu > 100 ? "Cores" : "Core";
|
||||
var cpuText = $"{Math.Round(CurrentStats.CpuUsage / Server.Cpu * 100, 1)}% / {Math.Round(Server.Cpu / 100D, 2)} {coreText}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-chip" Description="CPU Usage" Value="@cpuText"/>
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
@{
|
||||
string memoryHas;
|
||||
|
||||
if (Server.Memory >= 1024)
|
||||
memoryHas = $"{ByteSizeValue.FromMegaBytes(Server.Memory).GigaBytes} GB";
|
||||
else
|
||||
memoryHas = $"{Server.Memory} MB";
|
||||
|
||||
var memoryText = $"{Formatter.FormatSize(CurrentStats.MemoryUsage)} / {memoryHas}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-microchip" Description="Memory Usage" Value="@memoryText"/>
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
@{
|
||||
var networkText = $"{Formatter.FormatSize(CurrentStats.NetRead)} / {Formatter.FormatSize(CurrentStats.NetWrite)}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-transfer-alt" Description="Network Traffic (Read / Write)" Value="@networkText"/>
|
||||
</div>
|
||||
<div class="col-md-3 col-12">
|
||||
@{
|
||||
var ioText = $"{Formatter.FormatSize(CurrentStats.IoRead)} / {Formatter.FormatSize(CurrentStats.IoWrite)}";
|
||||
}
|
||||
|
||||
<StatCard Icon="bx-hdd" Description="IO Usage (Read / Write)" Value="@ioText"/>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-2">
|
||||
<div class="text-center fs-4 mt-2">CPU Usage</div>
|
||||
<div style="height: 25vh">
|
||||
<ServerStatsGraph Console="Console" Min="0" Max="100" Unit="%" Field1="@(x => x.CpuUsage / Server.Cpu * 100)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-2">
|
||||
<div class="text-center fs-4 mt-2">Memory Usage</div>
|
||||
<div style="height: 25vh">
|
||||
<ServerStatsGraph Console="Console" Min="0" Max="Server.Memory" Unit="MB" Field1="@(x => ByteSizeValue.FromBytes(x.MemoryUsage).MegaBytes)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-2">
|
||||
<div class="text-center fs-4 mt-2">Network Usage</div>
|
||||
<div style="height: 25vh">
|
||||
<ServerStatsGraph Console="Console" Min="0" Max="1024" Unit="KB" AllowDynamicView="true" Field1="@(x => ByteSizeValue.FromBytes(x.NetWrite).KiloBytes)" Field2="@(x => ByteSizeValue.FromBytes(x.NetRead).KiloBytes)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-2">
|
||||
<div class="text-center fs-4 mt-2">IO Usage</div>
|
||||
<div style="height: 25vh">
|
||||
<ServerStatsGraph Console="Console" Min="0" Max="1024" Unit="KB" Field1="@(x => x.IoWrite)" Field2="@(x => x.IoRead)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public ServerConsole Console { get; set; }
|
||||
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
private ServerStats CurrentStats = new();
|
||||
|
||||
private Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
Console.OnStatsChange += HandleStats;
|
||||
Console.OnStateChange += HandleState;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandleState(ServerState state)
|
||||
{
|
||||
if (state == ServerState.Offline || state == ServerState.Join2Start)
|
||||
{
|
||||
CurrentStats = new();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleStats(ServerStats stats)
|
||||
{
|
||||
CurrentStats = stats;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Console.OnStateChange -= HandleState;
|
||||
Console.OnStatsChange -= HandleStats;
|
||||
}
|
||||
}
|
||||
105
Moonlight/Features/Servers/UI/UserViews/Variables.razor
Normal file
105
Moonlight/Features/Servers/UI/UserViews/Variables.razor
Normal file
@@ -0,0 +1,105 @@
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
@using Moonlight.Features.Servers.UI.Components.VariableViews
|
||||
@using MoonCore.Abstractions
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using MoonCoreUI.Services
|
||||
@using Moonlight.Features.Servers.Entities.Enums
|
||||
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject Repository<ServerImage> ImageRepository
|
||||
@inject Repository<ServerVariable> ServerVariableRepository
|
||||
@inject ToastService ToastService
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="Load" ShowAsCard="true">
|
||||
<div class="row mt-1 g-5">
|
||||
<div class="col-md-6 col-12">
|
||||
<div class="card card-body p-5 h-100">
|
||||
<label class="form-label fs-5">Docker image</label>
|
||||
@if (Image.AllowDockerImageChange)
|
||||
{
|
||||
<SmartSelect @bind-Value="SelectedDockerImage"
|
||||
Items="Image.DockerImages"
|
||||
DisplayField="@(x => x.DisplayName)"
|
||||
OnChange="OnDockerImageChanged"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<select class="form-select disabled" disabled="disabled">
|
||||
<option>@SelectedDockerImage.DisplayName</option>
|
||||
</select>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@foreach (var variable in Server.Variables)
|
||||
{
|
||||
var imageVariable = Image.Variables.FirstOrDefault(x => x.Key == variable.Key);
|
||||
|
||||
if (imageVariable != null && imageVariable.AllowView)
|
||||
{
|
||||
<div class="d-flex flex-column col-md-3 col-12">
|
||||
<div class="card card-body p-5">
|
||||
<label class="form-label fs-5">@(imageVariable.DisplayName)</label>
|
||||
<div class="form-text text-gray-700 fs-5 mb-2 mt-0">
|
||||
@imageVariable.Description
|
||||
</div>
|
||||
<div class="mt-auto">
|
||||
@switch (imageVariable.Type)
|
||||
{
|
||||
case ServerImageVariableType.Number:
|
||||
<NumberVariableView Variable="variable" ImageVariable="imageVariable" OnChanged="Refresh"/>
|
||||
break;
|
||||
case ServerImageVariableType.Toggle:
|
||||
<ToggleVariableView Variable="variable" ImageVariable="imageVariable" OnChanged="Refresh"/>
|
||||
break;
|
||||
case ServerImageVariableType.Select:
|
||||
<SelectVariableView Variable="variable" ImageVariable="imageVariable" OnChanged="Refresh"/>
|
||||
break;
|
||||
default:
|
||||
<TextVariableView Variable="variable" ImageVariable="imageVariable" OnChanged="Refresh"/>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Server Server { get; set; }
|
||||
|
||||
private ServerImage Image;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private ServerDockerImage SelectedDockerImage;
|
||||
|
||||
private async Task Load(LazyLoader lazyLoader)
|
||||
{
|
||||
await lazyLoader.SetText("Loading server image data");
|
||||
|
||||
Image = ImageRepository
|
||||
.Get()
|
||||
.Include(x => x.Variables)
|
||||
.Include(x => x.DockerImages)
|
||||
.First(x => x.Id == Server.Image.Id);
|
||||
|
||||
if (Server.DockerImageIndex >= Image.DockerImages.Count || Server.DockerImageIndex == -1)
|
||||
SelectedDockerImage = Image.DockerImages.Last();
|
||||
else
|
||||
SelectedDockerImage = Image.DockerImages[Server.DockerImageIndex];
|
||||
}
|
||||
|
||||
private async Task OnDockerImageChanged()
|
||||
{
|
||||
Server.DockerImageIndex = Image.DockerImages.IndexOf(SelectedDockerImage);
|
||||
ServerRepository.Update(Server);
|
||||
|
||||
await ToastService.Success("Successfully changed docker image");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
|
||||
private async Task Refresh() => await LazyLoader.Reload();
|
||||
}
|
||||
Reference in New Issue
Block a user