New v2 project structure

This commit is contained in:
Marcel Baumgartner
2024-03-18 09:31:33 +01:00
parent e20415a5bd
commit 0a807605ad
727 changed files with 51204 additions and 0 deletions

View 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;
}
}

View 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;
}
}

View 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();
}
}

View 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();
}
}

View 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;
}
}

View 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();
}
}

View 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;
}
}

View 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();
}