Refactored ui. Improved console experience. Added command endpoint

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

View File

@@ -68,6 +68,7 @@ public class ServersController : Controller
.Include(x => x.Star)
.Skip(page * pageSize)
.Take(pageSize)
.OrderBy(x => x.Id)
.ToArrayAsync();
var mappedItems = items
@@ -213,7 +214,7 @@ public class ServersController : Controller
}
[HttpPatch("{id:int}")]
[Authorize(Policy = "permissions.admin.servers.write")]
[Authorize(Policy = "permissions:admin.servers.write")]
public async Task<ServerResponse> Update([FromRoute] int id, [FromBody] UpdateServerRequest request)
{
//TODO: Handle shrinking virtual disk
@@ -294,11 +295,16 @@ public class ServersController : Controller
.Include(x => x.Star)
.Include(x => x.Variables)
.Include(x => x.Backups)
.Include(x => x.Allocations)
.FirstOrDefaultAsync(x => x.Id == id);
if (server == null)
throw new HttpApiException("No server with that id found", 404);
server.Variables.Clear();
server.Backups.Clear();
server.Allocations.Clear();
try
{
// If the sync fails on the node and we aren't forcing the deletion,

View File

@@ -11,6 +11,7 @@ using MoonlightServers.ApiServer.Extensions;
using MoonlightServers.ApiServer.Models;
using MoonlightServers.ApiServer.Services;
using MoonlightServers.Shared.Enums;
using MoonlightServers.Shared.Http.Requests.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Allocations;
using MoonlightServers.Shared.Models;
@@ -293,6 +294,17 @@ public class ServersController : Controller
};
}
[HttpPost("{serverId:int}/command")]
public async Task RunCommand([FromRoute] int serverId, [FromBody] ServerCommandRequest request)
{
var server = await GetServerById(
serverId,
permission => permission is { Name: "console", Type: >= ServerPermissionType.ReadWrite }
);
await ServerService.RunCommand(server, request.Command);
}
private async Task<Server> GetServerById(int serverId, Func<ServerSharePermission, bool>? filter = null)
{
var server = await ServerRepository

View File

@@ -6,6 +6,7 @@ using MoonCore.Extended.Abstractions;
using MoonCore.Helpers;
using Moonlight.ApiServer.Database.Entities;
using MoonlightServers.ApiServer.Database.Entities;
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
namespace MoonlightServers.ApiServer.Services;
@@ -36,7 +37,7 @@ public class ServerService
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Stop(Server server)
{
try
@@ -49,7 +50,7 @@ public class ServerService
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task Kill(Server server)
{
try
@@ -64,7 +65,7 @@ public class ServerService
}
#endregion
public async Task Install(Server server)
{
try
@@ -90,7 +91,7 @@ public class ServerService
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task SyncDelete(Server server)
{
try
@@ -103,7 +104,7 @@ public class ServerService
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task<ServerStatusResponse> GetStatus(Server server)
{
try
@@ -137,7 +138,27 @@ public class ServerService
using var apiClient = await GetApiClient(server);
return await apiClient.GetJson<ServerStatsResponse>($"api/servers/{server.Id}/stats");
}
catch (HttpRequestException e)
catch (HttpRequestException)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
}
public async Task RunCommand(Server server, string command)
{
try
{
using var apiClient = await GetApiClient(server);
await apiClient.Post(
$"api/servers/{server.Id}/command",
new ServerCommandRequest()
{
Command = command
}
);
}
catch (HttpRequestException)
{
throw new HttpApiException("Unable to access the node the server is running on", 502);
}
@@ -149,10 +170,10 @@ public class ServerService
{
if (server.OwnerId == user.Id)
return true;
return PermissionHelper.HasPermission(user.Permissions, "admin.servers.get");
}
private async Task<HttpApiClient> GetApiClient(Server server)
{
var serverWithNode = server;

View File

@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using MoonCore.Exceptions;
using MoonlightServers.Daemon.ServerSystem.SubSystems;
using MoonlightServers.Daemon.Services;
using MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
using MoonlightServers.DaemonShared.DaemonSide.Http.Responses.Servers;
using MoonlightServers.DaemonShared.Enums;
@@ -10,7 +11,7 @@ namespace MoonlightServers.Daemon.Http.Controllers.Servers;
[Authorize]
[ApiController]
[Route("api/servers")]
[Route("api/servers/{serverId:int}")]
public class ServersController : Controller
{
private readonly ServerService ServerService;
@@ -20,19 +21,19 @@ public class ServersController : Controller
ServerService = serverService;
}
[HttpPost("{serverId:int}/sync")]
[HttpPost("sync")]
public async Task Sync([FromRoute] int serverId)
{
await ServerService.Sync(serverId);
}
[HttpDelete("{serverId:int}")]
[HttpDelete]
public async Task Delete([FromRoute] int serverId)
{
await ServerService.Delete(serverId);
}
[HttpGet("{serverId:int}/status")]
[HttpGet("status")]
public Task<ServerStatusResponse> GetStatus([FromRoute] int serverId)
{
var server = ServerService.Find(serverId);
@@ -48,7 +49,7 @@ public class ServersController : Controller
return Task.FromResult(result);
}
[HttpGet("{serverId:int}/logs")]
[HttpGet("logs")]
public async Task<ServerLogsResponse> GetLogs([FromRoute] int serverId)
{
var server = ServerService.Find(serverId);
@@ -65,7 +66,7 @@ public class ServersController : Controller
};
}
[HttpGet("{serverId:int}/stats")]
[HttpGet("stats")]
public Task<ServerStatsResponse> GetStats([FromRoute] int serverId)
{
var server = ServerService.Find(serverId);
@@ -85,4 +86,17 @@ public class ServersController : Controller
IoWrite = statsSubSystem.CurrentStats.IoWrite
});
}
[HttpPost("command")]
public async Task Command([FromRoute] int serverId, [FromBody] ServerCommandRequest request)
{
var server = ServerService.Find(serverId);
if (server == null)
throw new HttpApiException("No server with this id found", 404);
var consoleSubSystem = server.GetRequiredSubSystem<ConsoleSubSystem>();
await consoleSubSystem.WriteInput(request.Command);
}
}

View File

@@ -49,6 +49,13 @@
<_ContentIncludedByDefault Remove="storage\volumes\2\usercache.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\version_history.json" />
<_ContentIncludedByDefault Remove="storage\volumes\2\whitelist.json" />
<_ContentIncludedByDefault Remove="volumes\3\banned-ips.json" />
<_ContentIncludedByDefault Remove="volumes\3\banned-players.json" />
<_ContentIncludedByDefault Remove="volumes\3\ops.json" />
<_ContentIncludedByDefault Remove="volumes\3\plugins\spark\config.json" />
<_ContentIncludedByDefault Remove="volumes\3\usercache.json" />
<_ContentIncludedByDefault Remove="volumes\3\version_history.json" />
<_ContentIncludedByDefault Remove="volumes\3\whitelist.json" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace MoonlightServers.DaemonShared.DaemonSide.Http.Requests;
public class ServerCommandRequest
{
[Required(ErrorMessage = "The command is required")]
public string Command { get; set; }
}

View File

@@ -1,6 +1,7 @@
using MoonCore.Attributes;
using MoonCore.Helpers;
using MoonCore.Models;
using MoonlightServers.Shared.Http.Requests.Client.Servers;
using MoonlightServers.Shared.Http.Requests.Client.Servers.Variables;
using MoonlightServers.Shared.Http.Responses.Client.Servers;
using MoonlightServers.Shared.Http.Responses.Client.Servers.Variables;
@@ -59,6 +60,17 @@ public class ServerService
);
}
public async Task RunCommand(int serverId, string command)
{
await HttpApiClient.Post(
$"api/client/servers/{serverId}/command",
new ServerCommandRequest()
{
Command = command
}
);
}
public async Task<ServerWebSocketResponse> GetWebSocket(int serverId)
{
return await HttpApiClient.GetJson<ServerWebSocketResponse>(

View File

@@ -0,0 +1,89 @@
@using System.Text.Json.Serialization
@using Microsoft.Extensions.Logging
@using XtermBlazor
@inherits MoonCore.Blazor.FlyonUi.Modals.Components.BaseModal
@inject IJSRuntime JsRuntime
@inject ILogger<FullScreenModal> Logger
@implements IAsyncDisposable
<div class="bg-black p-2 relative w-full h-[90vh] rounded-lg">
@if (IsInitialized)
{
<Xterm @ref="Terminal"
Addons="Parent.Addons"
Options="Parent.Options"
Class="h-full w-full"
OnFirstRender="HandleFirstRender"/>
}
<div class="absolute top-4 right-4">
<button @onclick="Hide" class="btn btn-error btn-square">
<i class="icon-x text-lg"></i>
</button>
</div>
</div>
@code
{
[Parameter] public XtermConsole Parent { get; set; }
private bool IsInitialized = false;
private bool IsReadyToWrite = false;
private Xterm Terminal;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
// Initialize addons
try
{
await JsRuntime.InvokeVoidAsync("moonlightServers.loadAddons");
}
catch (Exception e)
{
Logger.LogError("An error occured while initializing addons: {e}", e);
}
// Subscribe to parent events
Parent.OnWrite += HandleWrite;
IsInitialized = true;
await InvokeAsync(StateHasChanged);
}
private async Task HandleFirstRender()
{
IsReadyToWrite = true;
try
{
await Terminal.Addon("addon-fit").InvokeVoidAsync("fit");
}
catch (Exception e)
{
Logger.LogError("An error occured while calling addons: {e}", e);
}
var outputToWrite = string.Concat(Parent.OutputCache.ToArray());
await Terminal.Write(outputToWrite);
}
private async Task HandleWrite(string content)
{
if (!IsReadyToWrite)
return;
await Terminal.Write(content);
}
public async ValueTask DisposeAsync()
{
Parent.OnWrite -= HandleWrite;
await Terminal.DisposeAsync();
}
}

View File

@@ -18,7 +18,7 @@
<div class="col-span-1">
<div class="card">
<div class="card-header">
Actions
<span class="card-title">Actions</span>
</div>
<div class="card-body">
<div class="flex flex-col gap-y-3">

View File

@@ -8,7 +8,7 @@
@inject NodeService NodeService
@inject ToastService ToastService
@inject ILogger<OverviewNodeUpdate> Logger
@inject ILogger<Overview> Logger
@implements IDisposable
@@ -26,7 +26,7 @@
</p>
<i class="icon-cpu text-4xl text-primary"></i>
</div>
<div class="text-base text-slate-300">
<div class="text-base text-base-content/90">
<span class="truncate">
CPU: @Statistics.Cpu.Model
</span>
@@ -42,7 +42,7 @@
</p>
<i class="icon-memory-stick text-4xl text-primary"></i>
</div>
<div class="text-base text-slate-300">
<div class="text-base text-base-content/90">
Memory
</div>
</div>
@@ -56,7 +56,7 @@
</div>
<i class="icon-shapes text-4xl text-primary"></i>
</div>
<div class="text-base text-slate-300">
<div class="text-base text-base-content/90">
Swap
</div>
</div>
@@ -76,7 +76,7 @@
var percentRounded = Math.Round(usage, 2);
<div class="flex flex-row items-center col-span-1">
<div class="text-sm text-slate-300 me-1.5 grow-0 flex flex-col">
<div class="text-sm text-base-content/90 me-1.5 grow-0 flex flex-col">
<span>#@(i)</span>
</div>
<div class="grow">
@@ -108,7 +108,7 @@
</div>
</div>
</div>
<div class="text-sm text-slate-300 mt-2.5 flex flex-col">
<div class="text-sm text-base-content/90 mt-2.5 flex flex-col">
<div>
Device: <span class="font-semibold">@disk.Device</span> - Mounted at: <span class="font-semibold truncate">@disk.MountPath</span>
</div>
@@ -136,7 +136,7 @@
</p>
<i class="icon-gallery-horizontal-end text-4xl text-primary"></i>
</div>
<div class="text-base text-slate-300">
<div class="text-base text-base-content/90">
Images
</div>
</div>
@@ -148,7 +148,7 @@
</p>
<i class="icon-container text-4xl text-primary"></i>
</div>
<div class="text-base text-slate-300">
<div class="text-base text-base-content/90">
Containers
</div>
</div>
@@ -160,7 +160,7 @@
</p>
<i class="icon-hard-hat text-4xl text-primary"></i>
</div>
<div class="text-base text-slate-300">
<div class="text-base text-base-content/90">
Build Cache
</div>
</div>

View File

@@ -8,8 +8,8 @@
@inject ILogger<ServerCard> Logger
@{
var gradient = "from-base-content/20";
var border = "border-base-content";
var gradient = "from-base-100/20";
var border = "border-base-content/80";
if (IsLoaded && !IsFailed)
{
@@ -20,7 +20,7 @@
ServerState.Starting => "from-warning/20",
ServerState.Stopping => "from-warning/20",
ServerState.Online => "from-success/20",
_ => "from-base-content/20"
_ => "from-base-100"
};
border = Status.State switch
@@ -30,13 +30,13 @@
ServerState.Starting => "border-warning",
ServerState.Stopping => "border-warning",
ServerState.Online => "border-success",
_ => "border-base-content"
_ => "border-base-content/80"
};
}
}
<a href="/servers/@Server.Id"
class="w-full bg-gradient-to-r @gradient to-base-content/75 to-25% px-5 py-3.5 rounded-xl border-l-8 @border">
class="w-full bg-gradient-to-r @gradient to-base-100/75 to-25% px-5 py-3.5 rounded-xl border-l-8 @border">
<div class="grid grid-cols-6">
<div class="flex items-center col-span-6 sm:col-span-2 2xl:col-span-1">
<div class="bg-base-content/10 bg-opacity-45 py-1 px-2 rounded-lg flex items-center">
@@ -54,7 +54,7 @@
Status.State is ServerState.Starting or ServerState.Stopping or ServerState.Online
)
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row">
<div>
<i class="icon-cpu"></i>
</div>
@@ -62,7 +62,7 @@
<div class="ms-3">@(Stats.CpuUsage)%</div>
</div>
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row">
<div>
<i class="icon-memory-stick"></i>
</div>
@@ -70,7 +70,7 @@
<div class="ms-3">@(Formatter.FormatSize(Stats.MemoryUsage)) / @(Formatter.FormatSize(ByteConverter.FromMegaBytes(Server.Memory).Bytes))</div>
</div>
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row">
<div>
<i class="icon-hard-drive"></i>
</div>
@@ -82,7 +82,7 @@
{
if (!IsLoaded)
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row text-gray-700">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row text-gray-700">
<div>
<i class="icon-loader"></i>
</div>
@@ -92,7 +92,7 @@
}
else if (IsFailed)
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row text-error">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row text-error">
<div>
<i class="icon-cable"></i>
</div>
@@ -102,7 +102,7 @@
}
else if (IsLoaded && !IsFailed && Status.State is ServerState.Offline)
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row text-error">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row text-error">
<div>
<i class="icon-power-off"></i>
</div>
@@ -112,7 +112,7 @@
}
else if (IsLoaded && !IsFailed && Status.State is ServerState.Installing)
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row text-primary">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row text-primary">
<div>
<i class="icon-hammer"></i>
</div>
@@ -127,7 +127,7 @@
@if (Server.Share != null)
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row col-span-2">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row col-span-2">
<div>
<i class="icon-share-2"></i>
</div>
@@ -137,7 +137,7 @@
}
else
{
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row">
<div>
<i class="icon-sparkles"></i>
</div>
@@ -145,7 +145,7 @@
<div class="ms-3">@Server.StarName</div>
</div>
<div class="bg-base-content/35 py-1 px-2 rounded-lg flex flex-row">
<div class="bg-base-200/75 py-1 px-2 rounded-lg flex flex-row">
<div>
<i class="icon-database"></i>
</div>

View File

@@ -1,9 +1,15 @@
@using Microsoft.AspNetCore.SignalR.Client
@using MoonlightServers.Frontend.Services
@inherits BaseServerTab
@inject ServerService ServerService
<div class="h-44">
<XtermConsole @ref="XtermConsole" OnAfterInitialized="OnAfterConsoleInitialized"/>
<XtermConsole @ref="XtermConsole"
OnAfterInitialized="OnAfterConsoleInitialized"
CommandHistory="Parent.CommandHistory"
OnCommand="OnCommand"/>
</div>
@code
@@ -16,10 +22,10 @@
HubConnection.On<string>("ConsoleOutput", async content =>
{
if(XtermConsole != null)
if (XtermConsole != null)
await XtermConsole.Write(content);
});
return Task.CompletedTask;
}
@@ -27,4 +33,7 @@
{
await XtermConsole!.Write(InitialConsoleMessage);
}
private async Task OnCommand(string command)
=> await ServerService.RunCommand(Server.Id, command + "\n");
}

View File

@@ -20,7 +20,7 @@
else
{
<WButton CssClasses="btn btn-primary" OnClick="Reinstall">
<i class="align-middle icon-hammer me-1"></i>
<i class="align-middle icon-hammer"></i>
<span class="align-middle">Reinstall</span>
</WButton>
}

View File

@@ -34,7 +34,7 @@
<div class="grid grid-col-1 gap-y-3">
@foreach (var share in Shares)
{
<div class="col-span-1 card card-body px-5 py-3 flex flex-row items-center justify-between">
<div class="col-span-1 card card-body py-3 flex flex-row items-center justify-between">
<div class="flex justify-start font-semibold">
<i class="icon-user-round me-2"></i>
<span>@share.Username</span>

View File

@@ -12,7 +12,7 @@
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
@foreach (var variable in Variables)
{
<div class="sm:col-span-2 card card-body p-5">
<div class="sm:col-span-2 card card-body">
<label class="block text-sm font-medium leading-6 text-base-content">
@variable.Name
</label>

View File

@@ -4,6 +4,7 @@
@using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations
@using MoonlightServers.Shared.Http.Responses.Admin.Servers
@using MoonCore.Blazor.FlyonUi.Forms
@using MoonlightServers.Frontend.UI.Views.Admin.All
@inject HttpApiClient ApiClient
@@ -12,7 +13,7 @@
<label class="block text-sm font-medium leading-6 text-base-content">Allocations</label>
<div class="mt-2">
<InputMultipleItem TItem="NodeAllocationResponse"
Value="Allocations"
Value="Parent.Allocations"
DisplayField="@(x => $"{x.IpAddress}:{x.Port}")"
SearchField="@(x => $"{x.IpAddress}:{x.Port}")"
ItemSource="Loader">
@@ -25,7 +26,7 @@
{
[Parameter] public UpdateServerRequest Request { get; set; }
[Parameter] public ServerResponse Server { get; set; }
[Parameter] public List<NodeAllocationResponse> Allocations { get; set; }
[Parameter] public Update Parent { get; set; }
private async Task<NodeAllocationResponse[]> Loader()
{

View File

@@ -3,6 +3,7 @@
@using MoonCore.Models
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonCore.Blazor.FlyonUi.Forms
@using MoonlightServers.Frontend.UI.Views.Admin.All
@inject HttpApiClient ApiClient
@@ -20,7 +21,7 @@
<InputItem TItem="UserResponse"
DisplayField="@(x => x.Username)"
SearchField="@(x => x.Username)"
@bind-Value="Owner"
@bind-Value="Parent.Owner"
ItemSource="Loader">
</InputItem>
@@ -61,7 +62,7 @@
@code
{
[Parameter] public UpdateServerRequest Request { get; set; }
[Parameter] public UserResponse Owner { get; set; }
[Parameter] public Update Parent { get; set; }
private async Task<UserResponse[]> Loader()
{

View File

@@ -11,7 +11,7 @@
<LazyLoader Load="Load">
<div class="grid grid-cols-1 gap-x-6 gap-y-8 sm:grid-cols-6">
@foreach (var variable in Variables)
@foreach (var variable in ServerVariables)
{
var reqVariable = Request.Variables.FirstOrDefault(x => x.Key == variable.Key);
var starVariable = StarVariables.FirstOrDefault(x => x.Key == variable.Key);
@@ -46,7 +46,7 @@
[Parameter] public ServerResponse Server { get; set; }
private StarVariableDetailResponse[] StarVariables;
private ServerVariableResponse[] Variables;
private ServerVariableResponse[] ServerVariables;
private async Task Load(LazyLoader _)
{
@@ -56,7 +56,7 @@
)
);
Variables = await PagedData<ServerVariableResponse>.All(async (page, pageSize) =>
ServerVariables = await PagedData<ServerVariableResponse>.All(async (page, pageSize) =>
await ApiClient.GetJson<PagedData<ServerVariableResponse>>(
$"api/admin/servers/{Server.Id}/variables?page={page}&pageSize={pageSize}"
)

View File

@@ -7,7 +7,7 @@
@using MoonlightServers.Shared.Http.Requests.Admin.Stars
@using MoonlightServers.Shared.Models
@inject ILogger<ParseConfigStarUpdate> Logger
@inject ILogger<ParseConfig> Logger
@inject ModalService ModalService
@inject AlertService AlertService
@inject ToastService ToastService

View File

@@ -20,7 +20,7 @@
<LazyLoader @ref="LazyLoader" Load="Load">
<div class="grid sm:grid-cols-2 xl:grid-cols-3 gap-4">
@foreach (var variable in Variables)
@foreach (var variable in CurrentVariables)
{
<div class="col-span-1 card card-body p-2.5">
<div class="flex items-center justify-between">
@@ -48,7 +48,7 @@
{
[Parameter] public StarDetailResponse Star { get; set; }
private StarVariableDetailResponse[] Variables;
private StarVariableDetailResponse[] CurrentVariables;
private LazyLoader LazyLoader;
private async Task Load(LazyLoader arg)
@@ -57,7 +57,7 @@
$"api/admin/servers/stars/{Star.Id}/variables?page=0&pageSize=50"
);
Variables = pagedVariables.Items;
CurrentVariables = pagedVariables.Items;
}
private async Task AddVariable()

View File

@@ -1,31 +1,72 @@
@using System.Collections.Concurrent
@using Microsoft.Extensions.Logging
@using MoonCore.Blazor.FlyonUi.Modals
@using MoonCore.Helpers
@using XtermBlazor
@using MoonCore.Blazor.FlyonUi.Components
@inject IJSRuntime JsRuntime
@inject ModalService ModalService
@inject ILogger<XtermConsole> Logger
<div class="bg-background rounded-lg p-2">
@implements IAsyncDisposable
<div class="bg-black rounded-lg p-2 relative">
@if (IsInitialized)
{
<Xterm @ref="Terminal"
Addons="Addons"
Options="Options"
Class="h-full w-full"
OnFirstRender="HandleFirstRender"/>
}
<div class="flex flex-row w-full mt-1.5">
<input @bind="CommandInput" @onkeyup="HandleKey" type="text" placeholder="Type here..." class="input grow"/>
<WButton OnClick="_ => SubmitCommand()" CssClasses="btn btn-square btn-primary grow-0 ms-1.5">
<i class="icon-send-horizontal text-lg"></i>
</WButton>
</div>
<div class="absolute top-4 right-4">
<div class="flex flex-col gap-y-1.5">
@if (IsPaused)
{
<button @onclick="TogglePause" class="btn btn-primary btn-square">
<i class="icon-play text-lg"></i>
</button>
}
else
{
<button @onclick="TogglePause" class="btn btn-secondary btn-square">
<i class="icon-pause text-lg"></i>
</button>
}
<button @onclick="OpenFullscreen" class="btn btn-secondary btn-square">
<i class="icon-maximize text-lg"></i>
</button>
</div>
</div>
</div>
@code
{
[Parameter] public Func<Task>? OnAfterInitialized { get; set; }
[Parameter] public Func<Task>? OnFirstRender { get; set; }
private Xterm Terminal;
private bool IsInitialized = false;
private bool HadFirstRender = false;
[Parameter] public bool ShowActions { get; set; } = true;
[Parameter] public bool ShowInput { get; set; } = false;
[Parameter] public int MaxOutputCacheSize { get; set; } = 250;
[Parameter] public Func<string, Task>? OnCommand { get; set; }
private readonly Queue<string> MessageCache = new();
private readonly HashSet<string> Addons = ["addon-fit"];
private readonly TerminalOptions Options = new()
[Parameter] public IList<string> CommandHistory { get; set; } = new ConcurrentList<string>();
public event Func<string, Task>? OnWrite;
private Xterm Terminal;
public HashSet<string> Addons { get; } = ["addon-fit"];
public TerminalOptions Options { get; } = new()
{
CursorBlink = false,
CursorStyle = CursorStyle.Bar,
@@ -36,17 +77,23 @@
Theme =
{
Background = "#000000"
}
},
};
public ConcurrentList<string> OutputCache { get; private set; } = new();
public ConcurrentList<string> WriteQueue { get; private set; } = new();
private int CommandIndex = -1;
private bool IsReadyToWrite = false;
private bool IsPaused = false;
private bool IsInitialized = false;
private string CommandInput = "";
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if(!firstRender)
if (!firstRender)
return;
// Initialize addons
try
{
await JsRuntime.InvokeVoidAsync("moonlightServers.loadAddons");
@@ -59,7 +106,7 @@
IsInitialized = true;
await InvokeAsync(StateHasChanged);
if(OnAfterInitialized != null)
if (OnAfterInitialized != null)
await OnAfterInitialized.Invoke();
}
@@ -74,13 +121,13 @@
Logger.LogError("An error occured while calling addons: {e}", e);
}
HadFirstRender = true;
IsReadyToWrite = true;
// Write out cache
while (MessageCache.Count > 0)
{
await Terminal.Write(MessageCache.Dequeue());
}
// Write queued content since initialisation started
var queueContent = string.Concat(WriteQueue);
WriteQueue.Clear();
await Terminal.Write(queueContent);
if (OnFirstRender != null)
await OnFirstRender.Invoke();
@@ -89,10 +136,103 @@
public async Task Write(string content)
{
// We cache messages here as there is the chance that the console isn't ready for input while receiving write tasks
if (HadFirstRender)
await Terminal.Write(content);
if (IsReadyToWrite && !IsPaused)
await HandleWrite(content);
else
MessageCache.Enqueue(content);
WriteQueue.Add(content);
}
private async Task HandleWrite(string content)
{
// Update output cache and prune it if required
if (OutputCache.Count > MaxOutputCacheSize)
{
for (var i = 0; i < 50; i++)
OutputCache.RemoveAt(i);
}
OutputCache.Add(content);
// Trigger events
if (OnWrite != null)
await OnWrite.Invoke(content);
// Write in own terminal
await Terminal.Write(content);
}
private async Task OpenFullscreen()
{
await ModalService.Launch<FullScreenModal>(parameters => { parameters["Parent"] = this; }, size: "max-w-none");
}
private async Task TogglePause()
{
IsPaused = !IsPaused;
await InvokeAsync(StateHasChanged);
if (IsPaused)
return;
var queueContent = string.Concat(WriteQueue);
WriteQueue.Clear();
await HandleWrite(queueContent);
}
private async Task SubmitCommand()
{
CommandHistory.Add(CommandInput);
if (OnCommand != null)
await OnCommand.Invoke(CommandInput);
CommandIndex = -1;
CommandInput = "";
await InvokeAsync(StateHasChanged);
}
private async Task HandleKey(KeyboardEventArgs keyboard)
{
switch (keyboard.Code)
{
case "Enter":
await SubmitCommand();
break;
case "ArrowUp" or "ArrowDown":
{
var highestIndex = CommandHistory.Count - 1;
if (CommandIndex == -1)
CommandIndex = highestIndex;
else
{
if (keyboard.Code is "ArrowUp")
CommandIndex++;
else if (keyboard.Code is "ArrowDown")
CommandIndex--;
}
if (CommandIndex > highestIndex)
CommandIndex = highestIndex;
if (CommandIndex < 0)
CommandIndex = 0;
if (CommandIndex <= highestIndex || CommandHistory.Count > 0)
CommandInput = CommandHistory[CommandIndex];
await InvokeAsync(StateHasChanged);
break;
}
}
}
public async ValueTask DisposeAsync()
{
await Terminal.DisposeAsync();
}
}

View File

@@ -5,7 +5,7 @@
@using MoonCore.Helpers
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonlightServers.Shared.Http.Requests.Admin.Servers
@using MoonlightServers.Frontend.UI.Components.Servers.CreateServerPartials
@using MoonlightServers.Frontend.UI.Components.Servers.CreatePartials
@using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes
@using MoonlightServers.Shared.Http.Responses.Admin.Stars
@@ -18,11 +18,11 @@
<PageHeader Title="Create Server">
<a href="/admin/servers/all" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>
@@ -31,16 +31,16 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<Tabs>
<Tab Name="General">
<GeneralServerCreate Request="Request" Parent="this" />
<General Request="Request" Parent="this" />
</Tab>
<Tab Name="Allocations">
<AllocationsServerCreate Request="Request" Parent="this" />
<Allocations Request="Request" Parent="this" />
</Tab>
<Tab Name="Variables">
<VariablesServerCreate Request="Request" Parent="this" />
<Variables Request="Request" Parent="this" />
</Tab>
<Tab Name="Advanced">
<AdvancedServerCreate Request="Request" />
<Advanced Request="Request" />
</Tab>
</Tabs>
</HandleForm>

View File

@@ -33,7 +33,13 @@
<Pagination TItem="ServerResponse" ItemSource="LoadData" />
<DataTableColumn TItem="ServerResponse" Field="@(x => x.Id)" Name="Id"/>
<DataTableColumn TItem="ServerResponse" Field="@(x => x.Name)" Name="Name"/>
<DataTableColumn TItem="ServerResponse" Field="@(x => x.Name)" Name="Name">
<ColumnTemplate>
<a class="text-primary" href="/admin/servers/all/update/@context.Id">
@context.Name
</a>
</ColumnTemplate>
</DataTableColumn>
<DataTableColumn TItem="ServerResponse" Field="@(x => x.NodeId)" Name="Node">
<ColumnTemplate>
@{

View File

@@ -6,7 +6,7 @@
@using Moonlight.Shared.Http.Responses.Admin.Users
@using MoonlightServers.Shared.Http.Requests.Admin.Servers
@using MoonlightServers.Shared.Http.Responses.Admin.Servers
@using MoonlightServers.Frontend.UI.Components.Servers.UpdateServerPartials
@using MoonlightServers.Frontend.UI.Components.Servers.UpdatePartials
@using MoonlightServers.Shared.Http.Responses.Admin.NodeAllocations
@inject HttpApiClient ApiClient
@@ -18,11 +18,11 @@
<LazyLoader Load="Load">
<PageHeader Title="Update Server">
<a href="/admin/servers/all" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -31,16 +31,16 @@
<HandleForm @ref="Form" Model="Request" OnValidSubmit="OnSubmit">
<Tabs>
<Tab Name="General">
<GeneralServerUpdate Request="Request" Owner="Owner"/>
<General Request="Request" Parent="this"/>
</Tab>
<Tab Name="Allocations">
<AllocationsServerUpdate Request="Request" Server="Server" Allocations="Allocations"/>
<Allocations Request="Request" Server="Server" Parent="this"/>
</Tab>
<Tab Name="Variables">
<VariablesServerUpdate Request="Request" Server="Server"/>
<Variables Request="Request" Server="Server"/>
</Tab>
<Tab Name="Advanced">
<AdvancedServerUpdate Request="Request"/>
<Advanced Request="Request"/>
</Tab>
</Tabs>
</HandleForm>
@@ -55,8 +55,8 @@
private UpdateServerRequest Request;
private ServerResponse Server;
private List<NodeAllocationResponse> Allocations = new();
private UserResponse Owner;
public List<NodeAllocationResponse> Allocations = new();
public UserResponse Owner;
private async Task Load(LazyLoader _)
{

View File

@@ -13,11 +13,11 @@
<PageHeader Title="Create Node">
<a href="/admin/servers/nodes" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>

View File

@@ -5,7 +5,7 @@
@using MoonCore.Helpers
@using MoonlightServers.Shared.Http.Requests.Admin.Nodes
@using MoonlightServers.Shared.Http.Responses.Admin.Nodes
@using MoonlightServers.Frontend.UI.Components.Nodes.UpdateNodePartials
@using MoonlightServers.Frontend.UI.Components.Nodes.UpdatePartials
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@@ -16,11 +16,11 @@
<LazyLoader Load="Load">
<PageHeader Title="@Node.Name">
<a href="/admin/servers/nodes" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -30,19 +30,19 @@
<Tabs>
<Tab Name="Overview">
<OverviewNodeUpdate Node="Node" />
<Overview Node="Node" />
</Tab>
<Tab Name="Settings">
<GeneralNodeUpdate Request="Request"/>
<General Request="Request"/>
</Tab>
<Tab Name="Allocations">
<AllocationsNodeUpdate Node="Node"/>
<Allocations Node="Node"/>
</Tab>
<Tab Name="Advanced Settings">
<AdvancedNodeUpdate Request="Request"/>
<Advanced Request="Request"/>
</Tab>
</Tabs>

View File

@@ -14,11 +14,11 @@
<PageHeader Title="Create Star">
<a href="/admin/servers/stars" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Create
</WButton>
</PageHeader>

View File

@@ -25,7 +25,7 @@
<PageHeader Title="Stars">
<InputFile id="import-file" hidden="" multiple OnChange="OnImportFiles"/>
<label for="import-file" class="btn btn-accent cursor-pointer">
<i class="icon-file-up mr-2"></i>
<i class="icon-file-up"></i>
Import
</label>

View File

@@ -5,7 +5,7 @@
@using MoonCore.Helpers
@using MoonlightServers.Shared.Http.Requests.Admin.Stars
@using MoonlightServers.Shared.Http.Responses.Admin.Stars
@using MoonlightServers.Frontend.UI.Components.Stars.UpdateStarPartials
@using MoonlightServers.Frontend.UI.Components.Stars.UpdatePartials
@inject HttpApiClient ApiClient
@inject NavigationManager Navigation
@@ -16,11 +16,11 @@
<LazyLoader Load="Load">
<PageHeader Title="Update Star">
<a href="/admin/servers/stars" class="btn btn-secondary">
<i class="icon-chevron-left mr-1"></i>
<i class="icon-chevron-left"></i>
Back
</a>
<WButton OnClick="_ => Form.Submit()" CssClasses="btn btn-primary">
<i class="icon-check mr-1"></i>
<i class="icon-check"></i>
Update
</WButton>
</PageHeader>
@@ -30,31 +30,31 @@
<Tabs>
<Tab Name="General">
<GeneralStarUpdate Request="Request" />
<General Request="Request" />
</Tab>
<Tab Name="Start, Stop & Status">
<StartStopStatusStarUpdate Request="Request" />
<StartStopStatus Request="Request" />
</Tab>
<Tab Name="Parse Configuration">
<ParseConfigStarUpdate Request="Request" />
<ParseConfig Request="Request" />
</Tab>
<Tab Name="Installation">
<InstallationStarUpdate Request="Request" />
<Installation Request="Request" />
</Tab>
<Tab Name="Variables">
<VariablesStarUpdate Star="Detail" />
<Variables Star="Detail" />
</Tab>
<Tab Name="Docker Images">
<DockerImageStarUpdate Star="Detail" />
<DockerImage Star="Detail" />
</Tab>
<Tab Name="Miscellaneous">
<MiscStarUpdate Star="Detail" Request="Request" />
<Misc Star="Detail" Request="Request" />
</Tab>
</Tabs>

View File

@@ -64,14 +64,14 @@
@if (State == ServerState.Offline)
{
<WButton CssClasses="btn btn-success" OnClick="_ => Start()">
<i class="icon-play me-1 align-middle"></i>
<i class="icon-play align-middle"></i>
<span class="align-middle">Start</span>
</WButton>
}
else
{
<button type="button" class="btn btn-success" disabled="disabled">
<i class="icon-play me-1 align-middle"></i>
<i class="icon-play align-middle"></i>
<span class="align-middle">Start</span>
</button>
}
@@ -79,14 +79,14 @@
@if (State == ServerState.Online)
{
<button type="button" class="btn btn-primary">
<i class="icon-rotate-ccw me-1 align-middle"></i>
<i class="icon-rotate-ccw align-middle"></i>
<span class="align-middle">Restart</span>
</button>
}
else
{
<button type="button" class="btn btn-primary" disabled="disabled">
<i class="icon-rotate-ccw me-1 align-middle"></i>
<i class="icon-rotate-ccw align-middle"></i>
<span class="align-middle">Restart</span>
</button>
}
@@ -96,14 +96,14 @@
if (State == ServerState.Stopping)
{
<WButton CssClasses="btn btn-error" OnClick="_ => Kill()">
<i class="icon-bomb me-1 align-middle"></i>
<i class="icon-bomb align-middle"></i>
<span class="align-middle">Kill</span>
</WButton>
}
else
{
<WButton CssClasses="btn btn-error" OnClick="_ => Stop()">
<i class="icon-squircle me-1 align-middle"></i>
<i class="icon-squircle align-middle"></i>
<span class="align-middle">Stop</span>
</WButton>
}
@@ -111,7 +111,7 @@
else
{
<button type="button" class="btn btn-error" disabled="disabled">
<i class="icon-squircle me-1 align-middle"></i>
<i class="icon-squircle align-middle"></i>
<span class="align-middle">Stop</span>
</button>
}
@@ -119,17 +119,17 @@
else
{
<button type="button" class="btn btn-success" disabled="disabled">
<i class="icon-play me-1 align-middle"></i>
<i class="icon-play align-middle"></i>
<span class="align-middle">Start</span>
</button>
<button type="button" class="btn btn-primary" disabled="disabled">
<i class="icon-rotate-ccw me-1 align-middle"></i>
<i class="icon-rotate-ccw align-middle"></i>
<span class="align-middle">Restart</span>
</button>
<button type="button" class="btn btn-error" disabled="disabled">
<i class="icon-squircle me-1 align-middle"></i>
<i class="icon-squircle align-middle"></i>
<span class="align-middle">Stop</span>
</button>
}
@@ -138,28 +138,20 @@
</div>
<div class="mt-3">
<ul class="flex flex-wrap -m-1">
<nav class="tabs space-x-2 overflow-x-auto" aria-label="Tabs" role="tablist" aria-orientation="horizontal">
@foreach (var tab in Tabs)
{
<li class="m-1">
@if (tab == CurrentTab)
{
<a href="/servers/@(ServerId)/@(tab.Path)"
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-transparent shadow-sm bg-gray-100 text-gray-800 transition">
@tab.Name
</a>
}
else
{
<a href="/servers/@(ServerId)/@(tab.Path)" @onclick:preventDefault
@onclick="() => SwitchTab(tab)"
class="inline-flex items-center justify-center text-sm font-medium leading-5 rounded-full px-3 py-1 border border-gray-700/60 hover:border-gray-600 shadow-sm bg-gray-800 text-base-content/60 transition">
@tab.Name
</a>
}
</li>
<a href="/servers/@(ServerId)/@(tab.Path)"
class="btn btn-text active-tab:bg-primary active-tab:text-primary-content hover:text-primary hover:bg-primary/20 bg-base-150 text-sm py-0.5 px-3 rounded-xl @(tab == CurrentTab ? "active" : "")"
data-tab="na"
role="tab"
@onclick:preventDefault
@onclick="() => SwitchTab(tab)">
@tab.Name
</a>
}
</ul>
</nav>
<div class="mt-5">
@if (CurrentTab == null)
@@ -201,6 +193,8 @@
private HubConnection? HubConnection;
public ConcurrentList<string> CommandHistory = new();
private async Task Load(LazyLoader _)
{
try

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace MoonlightServers.Shared.Http.Requests.Client.Servers;
public class ServerCommandRequest
{
[Required(ErrorMessage = "The command is required")]
public string Command { get; set; }
}