Compare commits

...
Sign in to create a new pull request.

42 commits

Author SHA1 Message Date
Masu Baumgartner
a4080cc1b1
Merge pull request #418 from Moonlight-Panel/v2_UpdateToDotnet8
Upgraded moonlight to dotnet 8
2024-05-27 14:32:46 +02:00
Masu Baumgartner
4f5a4913d7 Upgraded moonlight to dotnet 8 2024-05-27 14:32:16 +02:00
Masu Baumgartner
c340e48f02
Merge pull request #415 from Moonlight-Panel/v2_addNodeOnlineCheckOnServerDeploy
Added node online check on server create
2024-05-23 14:37:26 +02:00
Masu Baumgartner
257af8106d
Merge pull request #417 from Moonlight-Panel/v2_ImproveModularUI
Made user dashboard modular and extendable via plugins
2024-05-21 21:50:43 +02:00
Moritz
6eedc8aba9 Renamed the Implementation 2024-05-21 14:12:46 +02:00
Moritz
cba98bdf6f Made Index page Modular
Made Index Page editable with the Plugin System, added greeting Component
2024-05-20 09:07:06 +02:00
Marcel Baumgartner
d2da868b71 Updated to use the new mooncore version 2024-05-19 20:37:54 +02:00
Masu Baumgartner
685221b454
Merge pull request #416 from Moonlight-Panel/v2_ResponsivenessImprovements
Improved responsive behaviour
2024-05-19 15:33:08 +02:00
Moritz
f12e5f10d5 Improved Server Ui responsivity 2024-05-19 15:20:24 +02:00
Moritz
cc7b4d7daa Improved Responsiveness for auth pages 2024-05-19 14:52:42 +02:00
Moritz
769c876dc5 Added a way to sort the admin components with an index 2024-05-19 14:27:45 +02:00
Moritz
ccec79cca7 Added node online check on Server create 2024-05-19 00:04:27 +02:00
Moritz
e79f2199c3
Forgot two stars - readme.md 2024-05-18 23:52:09 +02:00
Moritz
c53d315bd8
Update README.md 2024-05-18 23:51:18 +02:00
Moritz
0ae9c27d93 Removed Last buggy thing
Forgot to remove it :(
2024-05-18 22:17:04 +02:00
Masu Baumgartner
2950034a30
Merge pull request #407 from Moonlight-Panel/v2_ImprovedAdminPage
Improved admin page with extendable columns and components via plugin system
2024-05-18 22:15:06 +02:00
Masu Baumgartner
158115bb3b
Merge branch 'v2' into v2_ImprovedAdminPage 2024-05-18 22:14:05 +02:00
Moritz
56184a8254 Final admin Page
Added Admin System To be as Modular as masu wants it to be
2024-05-18 22:11:58 +02:00
Masu Baumgartner
c27b1689f3
Merge pull request #410 from Moonlight-Panel/v2_AdminPageLinkFix
Fixed Linking and Cards on Admin Page
2024-05-17 08:42:01 +02:00
Moritz
3c3dd2af92
Made issue template v2 ready
Removed Wings Issue field, edited placeholders for version
2024-05-16 16:26:09 +02:00
Moritz
125260e7ef Fixed Linking and Cards on Admin Page 2024-05-16 16:21:39 +02:00
Marcel Baumgartner
b608a0779c Quick-fix for server loading in user view 2024-05-14 21:57:08 +02:00
Moritz
7225db0bf1 added some informations 2024-05-14 14:52:27 +02:00
Masu Baumgartner
3f0cdff262
Merge pull request #408 from Moonlight-Panel/v2_FileManagerImprovements
Adding some more file manager improvements to the main branch
2024-05-14 14:50:10 +02:00
Moritz
3270039a6a improved admin page
Added the logic for a more modular admin page, where every feature can add cards and components with just a call of a function
2024-05-14 14:43:54 +02:00
Marcel Baumgartner
fda972a90e Implemented archiving and extracting for users 2024-05-14 14:08:07 +02:00
Masu Baumgartner
45e81c98bf
Merge pull request #406 from Moonlight-Panel/v2_AddAdminPage
Added Basic Admin Page, will still be adding more in the future
2024-05-14 00:46:27 +02:00
Moritz
d9dd9bbf4d Optimized it, as Masu wished 2024-05-14 00:42:00 +02:00
Moritz
160de6443b Added Basic Admin Page, will still be adding more in the future 2024-05-13 18:38:52 +02:00
Masu Baumgartner
923a3c18b8 Started adding archive ui 2024-05-13 16:20:38 +02:00
Masu Baumgartner
8dc37525ce Improved egg importing so it should be able to import any egg now 2024-05-06 18:34:50 +02:00
Masu Baumgartner
7bd34842fa Commented out dummy value 2024-05-06 10:26:08 +02:00
Masu Baumgartner
da8b01bb98 Added server kill confirmation prompt. Improved power action handling 2024-05-06 10:24:45 +02:00
Masu Baumgartner
c9fe469f5b
Merge pull request #404 from Moonlight-Panel/v2_FileManagerImprovements
Improved file manager. This is the first merge of this branch. More commits are planned
2024-05-06 09:45:54 +02:00
Masu Baumgartner
406f7cad65 Added empty file list indicator. Removed dropzone.js 2024-05-06 09:44:38 +02:00
Masu Baumgartner
558e237608 Small fix for node memory calculation
Made calculation accurate using:
https://stackoverflow.com/questions/41224738/how-to-calculate-system-memory-usage-from-proc-meminfo-like-htop
2024-05-06 09:15:27 +02:00
Marcel Baumgartner
b4251a0f1f Started with adding modules for archiving for the file manager 2024-05-04 23:06:15 +02:00
Marcel Baumgartner
0234a8e179 Increased kestrel default limits and added option to change the moonlight defaults 2024-05-01 20:17:41 +02:00
Masu Baumgartner
efacaa9b86
Merge pull request #402 from Moonlight-Panel/v2_addFaviconWithAssetApi
V2 add favicon with asset api
2024-04-30 09:04:24 +02:00
Moritz
7c40d999ff Image already existed so i used that one 2024-04-29 16:34:11 +02:00
Moritz
99c14693d5 Added Favicon using the asset api 2024-04-29 16:19:54 +02:00
Marcel Baumgartner
2cf03d4b68 Improved MySQL container boot handling
This will reduce the large error messages to a single line saying that the mysql container is still booting. Hopefully this fixes the login cli command as well
2024-04-29 16:16:25 +02:00
47 changed files with 920 additions and 234 deletions

View file

@ -33,7 +33,7 @@ body:
attributes:
label: Panel Version
description: Version number of your Panel (latest is not a version)
placeholder: 1.4.0
placeholder: v2 EA
validations:
required: true
@ -42,16 +42,7 @@ body:
attributes:
label: Daemon Version
description: Version number of your Daemon (latest is not a version)
placeholder: 1.4.2
validations:
required: true
- type: input
id: wings-version
attributes:
label: Wings Version
description: Version number of your Wings (latest is not a version)
placeholder: 1.4.2
placeholder: v2 EA
validations:
required: true
@ -93,4 +84,4 @@ body:
- label: I have provided all relevant details, including the specific game and Docker images I am using if this issue is related to running a server.
required: true
- label: I have checked in the Discord server and believe this is a bug with the software, and not a configuration issue with my specific system.
required: true
required: true

3
.gitignore vendored
View file

@ -400,4 +400,5 @@ FodyWeavers.xsd
storage/
.idea/.idea.Moonlight/.idea/dataSources.xml
Moonlight/wwwroot/css/theme.css
Moonlight/wwwroot/css/theme.css.map
Moonlight/wwwroot/css/theme.css.map
.idea/.idea.Moonlight/.idea/discord.xml

View file

@ -3,5 +3,10 @@
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
<option name="theme" value="material" />
<option name="button1Title" value="" />
<option name="button1Url" value="" />
<option name="button2Title" value="" />
<option name="button2Url" value="" />
</component>
</project>

File diff suppressed because one or more lines are too long

View file

@ -39,6 +39,15 @@ public class CoreConfiguration
[Description("Specifies the location of the key .pem file to load")]
[JsonProperty("KeyPath")]
public string KeyPath { get; set; } = "";
[Description("Specifies the file upload limit per http request in megabytes")]
[JsonProperty("UploadLimit")]
public int UploadLimit { get; set; } = 100;
[Description(
"Specifies the maximum message size moonlight can receive via the websocket connection in kilobytes")]
[JsonProperty("MessageSizeLimit")]
public int MessageSizeLimit { get; set; } = 1024;
}
public class DatabaseData

View file

@ -3,18 +3,24 @@ using Microsoft.AspNetCore.Components;
using MoonCore.Abstractions;
using MoonCore.Helpers;
using MoonCore.Services;
using MoonCoreUI.Extensions;
using MoonCoreUI.Services;
using Moonlight.Core.Configuration;
using Moonlight.Core.Database;
using Moonlight.Core.Database.Entities;
using Moonlight.Core.Implementations.Diagnose;
using Moonlight.Core.Implementations.UI.Admin.AdminColumns;
using Moonlight.Core.Implementations.UI.Index;
using Moonlight.Core.Interfaces;
using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Interfaces.UI.User;
using Moonlight.Core.Models;
using Moonlight.Core.Models.Abstractions;
using Moonlight.Core.Models.Abstractions.Feature;
using Moonlight.Core.Models.Enums;
using Moonlight.Core.Repositories;
using Moonlight.Core.Services;
using Moonlight.Core.UI.Components.Cards;
namespace Moonlight.Core;
@ -55,24 +61,37 @@ public class CoreFeature : MoonlightFeature
builder.Services.AddScoped<ClipboardService>();
builder.Services.AddScoped<ModalService>();
// Configure interop
ToastService.Prefix = "moonlight.toasts";
ModalService.Prefix = "moonlight.modals";
AlertService.Prefix = "moonlight.alerts";
ClipboardService.Prefix = "moonlight.clipboard";
FileDownloadService.Prefix = "moonlight.utils";
builder.Services.AddMoonCoreUi(configuration =>
{
configuration.ToastJavascriptPrefix = "moonlight.toasts";
configuration.ModalJavascriptPrefix = "moonlight.modals";
configuration.AlertJavascriptPrefix = "moonlight.alerts";
configuration.ClipboardJavascriptPrefix = "moonlight.clipboard";
configuration.FileDownloadJavascriptPrefix = "moonlight.utils";
});
// Add external services and blazor/asp.net stuff
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddHttpContextAccessor();
builder.Services.AddControllers();
builder.Services.AddBlazorTable();
// Configure blazor pipeline in detail
builder.Services.AddServerSideBlazor().AddHubOptions(options =>
{
options.MaximumReceiveMessageSize = ByteSizeValue.FromKiloBytes(config.Http.MessageSizeLimit).Bytes;
});
// Setup authentication if required
if (config.Authentication.UseDefaultAuthentication)
builder.Services.AddScoped<IAuthenticationProvider, DefaultAuthenticationProvider>();
// Setup http upload limit
context.Builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = ByteSizeValue.FromMegaBytes(config.Http.UploadLimit).Bytes;
});
// Assets
// - Javascript
@ -103,6 +122,12 @@ public class CoreFeature : MoonlightFeature
// Define permissions
var permissionService = app.Services.GetRequiredService<PermissionService>();
await permissionService.Register(999, new()
{
Name = "See Admin Page",
Description = "Allows access to the admin page and the connected stats (server and user count)"
});
await permissionService.Register(1000, new()
{
Name = "Manage users",
@ -138,6 +163,10 @@ public class CoreFeature : MoonlightFeature
await pluginService.RegisterImplementation<IDiagnoseAction>(new FeatureDiagnoseAction());
await pluginService.RegisterImplementation<IDiagnoseAction>(new LogDiagnoseAction());
// UI
await pluginService.RegisterImplementation<IAdminDashboardColumn>(new UserCount());
await pluginService.RegisterImplementation<IUserDashboardComponent>(new GreetingMessages());
// Startup job services
var startupJobService = app.Services.GetRequiredService<StartupJobService>();

View file

@ -0,0 +1,20 @@
using MoonCoreUI.Helpers;
using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Models.Abstractions;
using Moonlight.Core.UI.Components.Cards;
namespace Moonlight.Core.Implementations.UI.Admin.AdminColumns;
public class UserCount : IAdminDashboardColumn
{
public Task<UiComponent> Get()
{
var res = new UiComponent()
{
Component = ComponentHelper.FromType<AdminUserCard>(),
Index = int.MinValue
};
return Task.FromResult(res);
}
}

View file

@ -0,0 +1,20 @@
using MoonCoreUI.Helpers;
using Moonlight.Core.Interfaces.UI.User;
using Moonlight.Core.Models.Abstractions;
using Moonlight.Core.UI.Components.Cards;
namespace Moonlight.Core.Implementations.UI.Index;
public class GreetingMessages : IUserDashboardComponent
{
public Task<UiComponent> Get()
{
var res = new UiComponent()
{
Component = ComponentHelper.FromType<TimeBasedGreetingMessages>(),
Index = int.MinValue
};
return Task.FromResult(res);
}
}

View file

@ -0,0 +1,8 @@
using Moonlight.Core.Models.Abstractions;
namespace Moonlight.Core.Interfaces.Ui.Admin;
public interface IAdminDashboardColumn
{
public Task<UiComponent> Get();
}

View file

@ -0,0 +1,8 @@
using Moonlight.Core.Models.Abstractions;
namespace Moonlight.Core.Interfaces.Ui.Admin;
public interface IAdminDashboardComponent
{
public Task<UiComponent> Get();
}

View file

@ -0,0 +1,8 @@
using Moonlight.Core.Models.Abstractions;
namespace Moonlight.Core.Interfaces.UI.User;
public interface IUserDashboardComponent
{
public Task<UiComponent> Get();
}

View file

@ -1,4 +1,9 @@
using System.Reflection;
using System.ComponentModel;
using System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Options;
using Moonlight.Core.UI.Components.Partials;
using IComponent = Microsoft.AspNetCore.Components.IComponent;
namespace Moonlight.Core.Models.Abstractions.Feature;
@ -6,7 +11,7 @@ public class UiInitContext
{
public List<SidebarItem> SidebarItems { get; set; } = new();
public List<Assembly> RouteAssemblies { get; set; } = new();
public void EnablePages<T>()
{
var assembly = typeof(T).Assembly;

View file

@ -0,0 +1,10 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.Core.Models.Abstractions;
public class UiComponent
{
public required RenderFragment Component { get; set; }
public int Index { get; set; } = 0;
}

View file

@ -14,7 +14,7 @@
<div class="d-flex justify-content-center">
<div class="d-flex align-items-center">
<div class="card card-body m-15 p-15">
<div class="card card-body m-8 m-md-15 p-8 p-md-15">
<div class="text-center mb-8">
<div class="fw-bold mb-3 fs-1">
Login

View file

@ -18,7 +18,7 @@
<div class="d-flex justify-content-center">
<div class="d-flex align-items-center">
<div class="card card-body m-15 p-15">
<div class="card card-body m-8 m-md-15 p-8 p-md-15">
@if (ConfigService.Get().Authentication.DenyRegister)
{
<IconAlert Color="danger" Icon="bx-shield-quarter" Title="Sign up disabled">

View file

@ -0,0 +1,19 @@
@using MoonCore.Abstractions
@using Moonlight.Core.Database.Entities
@inject Repository<User> UserRepository
<a href="/admin/servers">
<StatCard Value="@UserCount.ToString()" Description="Users" Icon="bxs-group"></StatCard>
</a>
@code {
private int UserCount;
protected override async Task OnInitializedAsync()
{
UserCount = UserRepository.Get().Count();
}
}

View file

@ -0,0 +1,57 @@
@using MoonCore.Services
@using Moonlight.Core.Configuration
@using Moonlight.Core.Services
@inject IdentityService IdentityService
@inject ConfigService<CoreConfiguration> ConfigService
<div class="card card-body p-8">
<div class="d-flex align-items-center">
<span class="fs-2 fw-semibold">
@{
var greeting = GetGreetingMessage();
}
@greeting.Item1
<span class="text-info">@IdentityService.CurrentUser.Username</span>
@greeting.Item2
</span>
</div>
</div>
@code {
// For explanation:
// The first value is the actual message
// end second value is for question marks and so on
//
// and yes, this "feature" is kinda useless but still i wanted to implement
// it. - Masu
private (string, string) GetGreetingMessage()
{
var config = ConfigService.Get().Customisation;
if (config.DisableTimeBasedGreetingMessages)
return ("Welcome back, ", "");
var time = DateTime.UtcNow.AddHours(config.GreetingTimezoneDifference);
if (time.Hour >= 23 || time.Hour < 5)
return ("\ud83d\ude34 Still awake, ", "?");
if (time.Hour >= 5 && time.Hour < 10)
return ("\ud83d\ude04 Good morning, ", "");
if (time.Hour >= 10 && time.Hour < 14)
return ("\u2600\ufe0f Have a nice day, ", "");
if (time.Hour >= 14 && time.Hour < 16)
return ("\ud83d\ude03 Good afternoon, ", "");
if (time.Hour >= 16 && time.Hour < 22)
return ("\ud83c\udf25\ufe0f Have a nice evening, ", "");
if (time.Hour >= 22 && time.Hour < 23)
return ("\ud83c\udf19 Sleep well, ", "");
return ("Welcome back ", "");
}
}

View file

@ -7,7 +7,7 @@
@inherits ErrorBoundaryBase
@if (Crashed || CurrentException != null)
@if (Crashed || Exception != null)
{
if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development" || (IdentityService.IsLoggedIn && IdentityService.CurrentUser.Permissions >= 9000))
{

View file

@ -11,14 +11,14 @@
else
{
<div class="row">
<div class="col-3 text-center d-flex align-items-center">
<div class="col-auto text-center d-flex align-items-center px-5">
<i class="bx bx-lg text-white align-middle @(Icon)"></i>
</div>
<div class="col-9">
<div class="col">
<div class="fw-semibold text-primary fs-2">
@Value
</div>
<div class="fs-4 mt-2">
<div class="fs-4 mt-2 text-white">
@Description
</div>
</div>

View file

@ -0,0 +1,56 @@
@page "/admin"
@using MoonCore.Abstractions
@using Moonlight.Core.Database.Entities
@using Moonlight.Core.Interfaces
@using Moonlight.Core.Interfaces.Ui.Admin
@using Moonlight.Core.Models.Abstractions
@using Moonlight.Core.Services
@using Moonlight.Features.Servers.Entities
@inject PluginService PluginService
@attribute [RequirePermission(999)]
<LazyLoader Load="Load">
<div class="row mb-8">
@foreach(var column in Columns.OrderBy(x => x.Index))
{
<div class="col-12 col-lg-6 col-xl">
@column.Component
</div>
}
</div>
@foreach (var component in Components.OrderBy(x => x.Index))
{
<div class="mb-4">
@component.Component
</div>
}
</LazyLoader>
@code {
private List<UiComponent> Columns = new();
private List<UiComponent> Components = new();
private async Task Load(LazyLoader arg)
{
await arg.SetText("Loading statistics...");
var componentImplementations = await PluginService.GetImplementations<IAdminDashboardComponent>();
foreach (var implementation in componentImplementations)
{
Components.Add(await implementation.Get());
}
var columnImplementations = await PluginService.GetImplementations<IAdminDashboardColumn>();
foreach (var implementation in columnImplementations)
{
Columns.Add(await implementation.Get());
}
}
}

View file

@ -1,60 +1,33 @@
@page "/"
@using Moonlight.Core.Interfaces.UI.User
@using Moonlight.Core.Models.Abstractions
@using Moonlight.Core.Services
@using MoonCore.Services
@using Moonlight.Core.Configuration
@inject IdentityService IdentityService
@inject ConfigService<CoreConfiguration> ConfigService
<div class="card card-body p-8">
<div class="d-flex align-items-center">
<span class="fs-2 fw-semibold">
@{
var greeting = GetGreetingMessage();
}
@greeting.Item1
<span class="text-info">@IdentityService.CurrentUser.Username</span>
@greeting.Item2
</span>
</div>
</div>
@inject PluginService PluginService
<LazyLoader Load="Load">
@foreach (var component in Components.OrderBy(x => x.Index))
{
<div class="mb-4">
@component.Component
</div>
}
</LazyLoader>
@code
{
// For explanation:
// The first value is the actual message
// end second value is for question marks and so on
//
// and yes, this "feature" is kinda useless but still i wanted to implement
// it. - Masu
private (string, string) GetGreetingMessage()
private List<UiComponent> Components = new();
private async Task Load(LazyLoader arg)
{
var config = ConfigService.Get().Customisation;
await arg.SetText("Loading...");
if (config.DisableTimeBasedGreetingMessages)
return ("Welcome back, ", "");
var implementations = await PluginService.GetImplementations<IUserDashboardComponent>();
var time = DateTime.UtcNow.AddHours(config.GreetingTimezoneDifference);
if (time.Hour >= 23 || time.Hour < 5)
return ("\ud83d\ude34 Still awake, ", "?");
if (time.Hour >= 5 && time.Hour < 10)
return ("\ud83d\ude04 Good morning, ", "");
if (time.Hour >= 10 && time.Hour < 14)
return ("\u2600\ufe0f Have a nice day, ", "");
if (time.Hour >= 14 && time.Hour < 16)
return ("\ud83d\ude03 Good afternoon, ", "");
if (time.Hour >= 16 && time.Hour < 22)
return ("\ud83c\udf25\ufe0f Have a nice evening, ", "");
if (time.Hour >= 22 && time.Hour < 23)
return ("\ud83c\udf19 Sleep well, ", "");
return ("Welcome back ", "");
foreach (var implementation in implementations)
{
Components.Add(await implementation.Get());
}
}
}

View file

@ -1,9 +1,9 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["Moonlight/Moonlight.csproj", "Moonlight/"]

View file

@ -26,7 +26,6 @@ public class FileManagerFeature : MoonlightFeature
var config = new ConfigService<CoreConfiguration>(PathBuilder.File("storage", "configs", "core.json"));
context.Builder.Services.AddSingleton(new JwtService<FileManagerJwtType>(config.Get().Security.Token));
context.AddAsset("FileManager", "js/dropzone.js");
context.AddAsset("FileManager", "js/filemanager.js");
context.AddAsset("FileManager", "editor/ace.css");
context.AddAsset("FileManager", "editor/ace.js");
@ -55,9 +54,12 @@ public class FileManagerFeature : MoonlightFeature
await pluginService.RegisterImplementation<IFileManagerContextAction>(new RenameContextAction());
await pluginService.RegisterImplementation<IFileManagerContextAction>(new MoveContextAction());
await pluginService.RegisterImplementation<IFileManagerContextAction>(new DownloadContextAction());
await pluginService.RegisterImplementation<IFileManagerContextAction>(new ArchiveContextAction());
await pluginService.RegisterImplementation<IFileManagerContextAction>(new ExtractContextAction());
await pluginService.RegisterImplementation<IFileManagerContextAction>(new DeleteContextAction());
await pluginService.RegisterImplementation<IFileManagerSelectionAction>(new MoveSelectionAction());
await pluginService.RegisterImplementation<IFileManagerSelectionAction>(new ArchiveSelectionAction());
await pluginService.RegisterImplementation<IFileManagerSelectionAction>(new DeleteSelectionAction());
await pluginService.RegisterImplementation<IFileManagerCreateAction>(new CreateFileAction());

View file

@ -0,0 +1,59 @@
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
namespace Moonlight.Features.FileManager.Implementations;
public class ArchiveContextAction : IFileManagerContextAction
{
public string Name => "Archive";
public string Icon => "bxs-archive-in";
public string Color => "warning";
public Func<FileEntry, bool> Filter => _ => true;
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry,
IServiceProvider provider)
{
var archiveAccess = access.Actions as IArchiveFileActions;
if (archiveAccess == null)
throw new DisplayException("This file access does not support archiving");
var alertService = provider.GetRequiredService<AlertService>();
var fileName = await alertService.Text("Enter the archive file name", "",
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz");
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return;
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient");
try
{
await archiveAccess.Archive(
access.CurrentDirectory + fileName,
new[] { access.CurrentDirectory + entry.Name }
);
await toastService.Success("Successfully created archive");
}
catch (Exception e)
{
Logger.Warn($"An error occured while archiving item ({entry.Name}):");
Logger.Warn(e);
await toastService.Danger("An unknown error occured while creating archive");
}
finally
{
await toastService.RemoveProgress("fileManagerArchive");
}
await fileManager.View.Refresh();
}
}

View file

@ -0,0 +1,55 @@
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
namespace Moonlight.Features.FileManager.Implementations;
public class ArchiveSelectionAction : IFileManagerSelectionAction
{
public string Name => "Archive";
public string Color => "primary";
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry[] entries,
IServiceProvider provider)
{
var archiveAccess = access.Actions as IArchiveFileActions;
if (archiveAccess == null)
throw new DisplayException("This file access does not support archiving");
var alertService = provider.GetRequiredService<AlertService>();
var fileName = await alertService.Text("Enter the archive file name", "",
Formatter.FormatDate(DateTime.UtcNow) + ".tar.gz");
if (string.IsNullOrEmpty(fileName) || fileName.Contains("..")) // => canceled
return;
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerArchive", "Archiving... Please be patient");
try
{
await archiveAccess.Archive(
access.CurrentDirectory + fileName,
entries.Select(x => access.CurrentDirectory + x.Name).ToArray()
);
await toastService.Success("Successfully created archive");
}
catch (Exception e)
{
Logger.Warn($"An error occured while archiving items ({entries.Length}):");
Logger.Warn(e);
await toastService.Danger("An unknown error occured while creating archive");
}
finally
{
await toastService.RemoveProgress("fileManagerArchive");
}
}
}

View file

@ -0,0 +1,52 @@
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonCoreUI.Services;
using Moonlight.Features.FileManager.Interfaces;
using Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
namespace Moonlight.Features.FileManager.Implementations;
public class ExtractContextAction : IFileManagerContextAction
{
public string Name => "Extract";
public string Icon => "bxs-archive-out";
public string Color => "warning";
public Func<FileEntry, bool> Filter => entry => entry.IsFile && entry.Name.EndsWith(".tar.gz");
public async Task Execute(BaseFileAccess access, UI.Components.FileManager fileManager, FileEntry entry, IServiceProvider provider)
{
var archiveAccess = access.Actions as IArchiveFileActions;
if (archiveAccess == null)
throw new DisplayException("This file access does not support archiving");
await fileManager.OpenFolderSelect("Select where you want to extract the content of the archive", async destination =>
{
var toastService = provider.GetRequiredService<ToastService>();
await toastService.CreateProgress("fileManagerExtract", "Extracting... Please be patient");
try
{
await archiveAccess.Extract(
access.CurrentDirectory + entry.Name,
destination
);
await toastService.Success("Successfully extracted archive");
}
catch (Exception e)
{
Logger.Warn($"An error occured while extracting archive ({entry.Name}):");
Logger.Warn(e);
await toastService.Danger("An unknown error occured while extracting archive");
}
finally
{
await toastService.RemoveProgress("fileManagerExtract");
}
await fileManager.View.Refresh();
});
}
}

View file

@ -4,9 +4,9 @@ namespace Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
public class BaseFileAccess : IDisposable
{
private readonly IFileActions Actions;
public readonly IFileActions Actions;
private string CurrentDirectory = "/";
public string CurrentDirectory { get; private set; } = "/";
public BaseFileAccess(IFileActions actions)
{

View file

@ -0,0 +1,7 @@
namespace Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
public interface IArchiveFileActions
{
public Task Archive(string path, string[] files);
public Task Extract(string path, string destination);
}

View file

@ -1,7 +0,0 @@
namespace Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
public interface IFileCompressAccess
{
public Task Compress(string[] names);
public Task Decompress(string name);
}

View file

@ -1,6 +0,0 @@
namespace Moonlight.Features.FileManager.Models.Abstractions.FileAccess;
public interface IFileLaunchAccess
{
public Task<string> GetLaunchUrl();
}

View file

@ -30,7 +30,11 @@ public class FileManagerInteropService
public async Task UpdateUrl(string urlId, string url)
{
await JsRuntime.InvokeVoidAsync("filemanager.updateUrl", urlId, url);
try
{
await JsRuntime.InvokeVoidAsync("filemanager.updateUrl", urlId, url);
}
catch (TaskCanceledException) { /* ignored */ }
}
[JSInvokable]

View file

@ -95,7 +95,8 @@ else
OnEntryClicked="OnEntryClicked"
OnNavigateUpClicked="OnNavigateUpClicked"
OnSelectionChanged="OnSelectionChanged"
EnableContextMenu="true">
EnableContextMenu="true"
ShowUploadPrompt="true">
<ContextMenuTemplate>
@foreach (var action in ContextActions)
{

View file

@ -209,6 +209,15 @@
}
</tbody>
</table>
@if (Entries.Length == 0 && ShowUploadPrompt)
{
<div class="py-4">
<IconAlert Color="primary" Title="No files and folders found" Icon="bx-cloud-upload">
Drag and drop files and folders here to start uploading them or click on the upload button on the top
</IconAlert>
</div>
}
</div>
@if (EnableContextMenu && ContextMenuTemplate != null)
@ -231,6 +240,7 @@
[Parameter] public bool ShowDate { get; set; } = true;
[Parameter] public bool ShowSelect { get; set; } = true;
[Parameter] public bool ShowNavigateUp { get; set; } = true;
[Parameter] public bool ShowUploadPrompt { get; set; } = false;
[Parameter] public RenderFragment<FileEntry>? ContextMenuTemplate { get; set; }
[Parameter] public bool EnableContextMenu { get; set; } = false;

View file

@ -1,6 +1,11 @@
using System.ComponentModel;
using Newtonsoft.Json;
namespace Moonlight.Features.Servers.Configuration;
public class ServersConfiguration
{
[JsonProperty("DisableServerKillWarning")]
[Description("With this option you can globally disable the confirmation popup shown when killing a server")]
public bool DisableServerKillWarning { get; set; } = false;
}

View file

@ -9,6 +9,7 @@ using Moonlight.Features.Servers.Entities;
using Moonlight.Features.Servers.Models;
using Moonlight.Features.Servers.Models.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Moonlight.Features.Servers.Helpers;
@ -30,7 +31,7 @@ public class ImageConversionHelper
.Include(x => x.Variables)
.First(x => x.Id == image.Id);
var model = new ImageJson()
{
Name = imageWithData.Name,
@ -54,7 +55,7 @@ public class ImageConversionHelper
foreach (var variable in imageWithData.Variables)
model.Variables.Add(Mapper.Map<ImageJson.ImageVariable>(variable));
foreach (var dockerImage in imageWithData.DockerImages)
model.DockerImages.Add(Mapper.Map<ImageJson.ImageDockerImage>(dockerImage));
@ -88,20 +89,21 @@ public class ImageConversionHelper
foreach (var variable in model.Variables)
result.Variables.Add(Mapper.Map<ServerImageVariable>(variable));
foreach (var dockerImage in model.DockerImages)
result.DockerImages.Add(Mapper.Map<ServerDockerImage>(dockerImage));
return Task.FromResult(result);
}
public Task<ServerImage> ImportFromEggJson(string json)
// Old import function which used the microsoft json parsing
public Task<ServerImage> ImportFromEggJson_Old(string json)
{
var fixedJson = json;
fixedJson = fixedJson.Replace("\\/", "/");
// Note: We use microsofts config system here as its dynamic and probably the best parsing method to use here
// Note: We use microsofts config system instead of newtonsoft.json as its dynamic and probably the best parsing method to use here
var eggData = new ConfigurationBuilder()
.AddJsonString(fixedJson)
.Build();
@ -114,7 +116,11 @@ public class ImageConversionHelper
result.Author = eggData["author"] ?? "Author was missing";
result.StartupCommand = eggData["startup"] ?? "Startup was missing";
result.StopCommand = eggData.GetSection("config")["stop"] ?? "Stop command was missing";
// Some weird eggs use ^^C in as a stop command, so we need to handle this as well
// because moonlight handles power signals correctly, wings does/did not
result.StopCommand = result.StopCommand.Replace("^^C", "^C");
// Startup detection
var startupDetectionData = new ConfigurationBuilder()
.AddJsonString(eggData.GetSection("config")["startup"] ?? "{}")
@ -122,14 +128,14 @@ public class ImageConversionHelper
// Node Regex: As the online detection uses regex, we want to escape any special chars from egg imports
// as eggs dont use regex and as such may contain characters which regex uses as meta characters.
// Without this escaping, many startup detection string wont work
// Without this escaping, many startup detection strings wont work
result.OnlineDetection = Regex.Escape(startupDetectionData["done"] ?? "Online detection was missing");
// Docker image method 1:
// Reference egg: https://github.com/parkervcp/eggs/blob/master/game_eggs/mindustry/egg-mindustry.json
if (eggData["image"] != null)
{
result.DockerImages.Add(new ()
result.DockerImages.Add(new()
{
Name = eggData["image"]!,
AutoPull = true,
@ -143,14 +149,14 @@ public class ImageConversionHelper
foreach (var dockerImage in dockerImagesSection.GetChildren())
{
result.DockerImages.Add(new ()
result.DockerImages.Add(new()
{
Name = dockerImage.Value ?? "Docker image name was missing",
AutoPull = true,
DisplayName = dockerImage.Key
});
}
// Docker image method 3:
// Reference egg: https://github.com/parkervcp/eggs/blob/master/game_eggs/minecraft/java/cuberite/egg-cuberite.json
var dockerImagesList = eggData.GetValue<List<string>>("images");
@ -159,7 +165,7 @@ public class ImageConversionHelper
{
foreach (var imageName in dockerImagesList)
{
result.DockerImages.Add(new ()
result.DockerImages.Add(new()
{
Name = imageName,
AutoPull = true,
@ -167,14 +173,14 @@ public class ImageConversionHelper
});
}
}
// Parse config
var parseConfigData = new ConfigurationBuilder()
.AddJsonString(eggData.GetSection("config")["files"] ?? "{}")
.Build();
var parseConfigModels = new List<ServerParseConfig>();
foreach (var fileSection in parseConfigData.GetChildren())
{
var model = new ServerParseConfig()
@ -189,20 +195,23 @@ public class ImageConversionHelper
valueWithChecks = valueWithChecks.Replace("server.build.default.port", "SERVER_PORT");
valueWithChecks = valueWithChecks.Replace("server.build.env.", "");
model.Configuration.Add(findSection.Key, valueWithChecks);
}
parseConfigModels.Add(model);
}
result.ParseConfiguration = JsonConvert.SerializeObject(parseConfigModels);
// Installation
result.InstallShell = "/bin/" + (eggData.GetSection("scripts").GetSection("installation")["entrypoint"] ?? "Install shell was missing");
result.InstallScript = eggData.GetSection("scripts").GetSection("installation")["script"] ?? "Install script was missing";
result.InstallDockerImage = eggData.GetSection("scripts").GetSection("installation")["container"] ?? "Install script was missing";
result.InstallShell = "/bin/" + (eggData.GetSection("scripts").GetSection("installation")["entrypoint"] ??
"Install shell was missing");
result.InstallScript = eggData.GetSection("scripts").GetSection("installation")["script"] ??
"Install script was missing";
result.InstallDockerImage = eggData.GetSection("scripts").GetSection("installation")["container"] ??
"Install script was missing";
// Variables
foreach (var variableSection in eggData.GetSection("variables").GetChildren())
{
@ -225,10 +234,159 @@ public class ImageConversionHelper
variable.AllowView = variableSection.GetValue<int>("user_viewable") == 1;
variable.AllowEdit = variableSection.GetValue<int>("user_editable") == 1;
}
result.Variables.Add(variable);
}
return Task.FromResult(result);
}
public Task<ServerImage> ImportFromEggJson(string json)
{
// Prepare json
var fixedJson = json;
fixedJson = fixedJson.Replace("\\/", "/");
// Prepare result object and set moonlight native fields
var result = new ServerImage();
result.AllocationsNeeded = 1;
result.AllowDockerImageChange = true;
//
var egg = JObject.Parse(fixedJson);
result.AllocationsNeeded = 1; // We cannot convert this value as its moonlight native
// Basic values
result.Name = egg["name"]?.Value<string>() ?? "Name was missing";
result.Author = egg["author"]?.Value<string>() ?? "Author was missing";
result.StartupCommand = egg["startup"]?.Value<string>() ?? "Startup was missing";
result.StopCommand = egg["config"]?["stop"]?.Value<string>() ?? "Stop command was missing";
// Some weird eggs use ^^C in as a stop command, so we need to handle this as well
// because moonlight handles power signals correctly, wings does/did not
result.StopCommand = result.StopCommand.Replace("^^C", "^C");
// Startup detection
var startup = JObject.Parse(egg["config"]?["startup"]?.Value<string>() ?? "{}");
// Node Regex: As the online detection uses regex, we want to escape any special chars from egg imports
// as eggs dont use regex and as such may contain characters which regex uses as meta characters.
// Without this escaping, many startup detection strings wont work
result.OnlineDetection = Regex.Escape(startup["done"]?.Value<string>() ?? "Online detection was missing");
// Docker images
// Docker image method 1:
// Reference egg: https://github.com/parkervcp/eggs/blob/master/game_eggs/mindustry/egg-mindustry.json
if (egg["image"] != null)
{
result.DockerImages.Add(new()
{
Name = egg["image"]?.Value<string>() ?? "Docker image not specified",
AutoPull = true,
DisplayName = egg["image"]?.Value<string>() ?? "Docker image not specified"
});
}
// Docker image method 2:
// Reference egg: https://github.com/parkervcp/eggs/blob/master/game_eggs/minecraft/java/cuberite/egg-cuberite.json
if (egg["images"] != null)
{
var images = egg["images"]?.ToObject<JArray>() ?? JArray.Parse("[]");
foreach (var imageName in images)
{
result.DockerImages.Add(new()
{
Name = imageName.Value<string>() ?? "Docker image name not found",
AutoPull = true,
DisplayName = imageName.Value<string>() ?? "Docker image name not found",
});
}
}
// Docker image method 3:
// Reference egg: https://github.com/parkervcp/eggs/blob/master/game_eggs/minecraft/java/paper/egg-paper.json
if (egg["docker_images"] != null)
{
var images = egg["docker_images"]?.ToObject<JObject>() ?? JObject.Parse("{}");
foreach (var kvp in images)
{
result.DockerImages.Add(new()
{
Name = kvp.Value?.Value<string>() ?? kvp.Key,
AutoPull = true,
DisplayName = kvp.Key
});
}
}
// Parse config
var parseConfig = JObject.Parse(egg["config"]?["files"]?.Value<string>() ?? "{}");
var parseConfigModels = new List<ServerParseConfig>();
foreach (var config in parseConfig)
{
var model = new ServerParseConfig()
{
File = config.Key,
Type = config.Value?["parser"]?.Value<string>() ?? "parser was missing"
};
if (config.Value?["find"] == null)
continue;
foreach (var findSection in config.Value!["find"]!.ToObject<JObject>() ?? JObject.Parse("{}"))
{
var valueWithChecks = findSection.Value?.Value<string>() ?? "Find value was null";
valueWithChecks = valueWithChecks.Replace("server.build.default.port", "SERVER_PORT");
valueWithChecks = valueWithChecks.Replace("server.build.env.", "");
model.Configuration.Add(findSection.Key, valueWithChecks);
}
parseConfigModels.Add(model);
}
result.ParseConfiguration = JsonConvert.SerializeObject(parseConfigModels);
// Installation
var installation = egg["scripts"]?["installation"] ?? JObject.Parse("{}");
result.InstallShell = "/bin/" + installation.Value<string>("entrypoint") ?? "Install shell was missing";
result.InstallScript = installation.Value<string>("script") ?? "Install script was missing";
result.InstallDockerImage = installation.Value<string>("container") ?? "Install container was missing";
// Variables
foreach (var variableSection in egg["variables"]?.Children() ?? JEnumerable<JToken>.Empty)
{
var variable = new ServerImageVariable()
{
DisplayName = variableSection.Value<string>("name") ?? "Name was missing",
Description = variableSection.Value<string>("description") ?? "Description was missing",
Key = variableSection.Value<string>("env_variable") ?? "Environment variable was missing",
DefaultValue = variableSection.Value<string>("default_value") ?? "Default value was missing",
};
// Check if it is a bool value
if (bool.TryParse(variableSection["user_viewable"]?.Value<string>(), out _))
{
variable.AllowView = variableSection.Value<bool>("user_viewable");
variable.AllowEdit = variableSection.Value<bool>("user_editable");
}
else
{
variable.AllowView = variableSection.Value<int>("user_viewable") == 1;
variable.AllowEdit = variableSection.Value<int>("user_editable") == 1;
}
result.Variables.Add(variable);
}
return Task.FromResult(result);
}
}

View file

@ -4,7 +4,7 @@ using Moonlight.Features.Servers.Exceptions;
namespace Moonlight.Features.Servers.Helpers;
public class ServerApiFileActions : IFileActions
public class ServerApiFileActions : IFileActions, IArchiveFileActions
{
private readonly string Endpoint;
private readonly string Token;
@ -43,6 +43,16 @@ public class ServerApiFileActions : IFileActions
public async Task WriteFileStream(string path, Stream dataStream) =>
await ApiClient.PostFile($"writeFileStream?path={path}", dataStream, "upload");
public async Task Archive(string path, string[] files)
{
await ApiClient.Post($"archive?path={path}&provider=tar.gz", files);
}
public async Task Extract(string path, string destination)
{
await ApiClient.Post($"extract?path={path}&destination={destination}&provider=tar.gz");
}
public IFileActions Clone() => new ServerApiFileActions(Endpoint, Token, ServerId);
public void Dispose() => ApiClient.Dispose();

View file

@ -0,0 +1,19 @@
using MoonCoreUI.Helpers;
using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Models.Abstractions;
using Moonlight.Features.Servers.UI.Components.Cards;
namespace Moonlight.Features.Servers.Implementations.UI.Admin.AdminColumns;
public class ServerCount : IAdminDashboardColumn
{
public Task<UiComponent> Get()
{
var res = new UiComponent()
{
Component = ComponentHelper.FromType<AdminServersCard>()
};
return Task.FromResult(res);
}
}

View file

@ -2,13 +2,17 @@ using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.Core.Configuration;
using Moonlight.Core.Interfaces;
using Moonlight.Core.Interfaces.Ui.Admin;
using Moonlight.Core.Models.Abstractions.Feature;
using Moonlight.Core.Services;
using Moonlight.Features.Servers.Actions;
using Moonlight.Features.Servers.Configuration;
using Moonlight.Features.Servers.Http.Middleware;
using Moonlight.Features.Servers.Implementations.Diagnose;
using Moonlight.Features.Servers.Implementations.UI.Admin.AdminColumns;
using Moonlight.Features.Servers.Models.Enums;
using Moonlight.Features.Servers.Services;
using Moonlight.Features.Servers.UI.Components.Cards;
namespace Moonlight.Features.Servers;
@ -29,6 +33,11 @@ public class ServersFeature : MoonlightFeature
var config = new ConfigService<CoreConfiguration>(PathBuilder.File("storage", "configs", "core.json"));
context.Builder.Services.AddSingleton(new JwtService<ServersJwtType>(config.Get().Security.Token));
//
var configService = new ConfigService<ServersConfiguration>(PathBuilder.File("storage", "configs", "servers.json"));
context.Builder.Services.AddSingleton(configService);
// Assets
context.AddAsset("Servers", "css/XtermBlazor.css");
context.AddAsset("Servers", "css/apexcharts.css");
@ -89,6 +98,8 @@ public class ServersFeature : MoonlightFeature
var pluginService = app.Services.GetRequiredService<PluginService>();
await pluginService.RegisterImplementation<IDiagnoseAction>(new NodesDiagnoseAction());
await pluginService.RegisterImplementation<IAdminDashboardColumn>(new ServerCount());
}
public override Task OnUiInitialized(UiInitContext context)

View file

@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using MoonCore.Abstractions;
using MoonCore.Attributes;
using MoonCore.Exceptions;
using MoonCore.Helpers;
using MoonCore.Services;
using Moonlight.Core.Configuration;
using Moonlight.Core.Database.Entities;
@ -22,6 +23,8 @@ public class ServerService
public ServerConsoleService Console => ServiceProvider.GetRequiredService<ServerConsoleService>();
public ServerBackupService Backup => ServiceProvider.GetRequiredService<ServerBackupService>();
public ServerScheduleService Schedule => ServiceProvider.GetRequiredService<ServerScheduleService>();
public NodeService NodeService => ServiceProvider.GetRequiredService<NodeService>();
private readonly IServiceProvider ServiceProvider;
@ -74,6 +77,19 @@ public class ServerService
// Load node
var node = nodeRepo.Get().First(x => x.Id == form.Node.Id);
// Check if node is available
try
{
await NodeService.GetStatus(node);
}
catch (Exception e)
{
Logger.Warn($"Could not establish to the node with the id {node.Id}");
Logger.Warn(e);
throw new DisplayException($"Could not establish connection to the node: {e.Message}");
}
// Load user
var user = userRepo.Get().First(x => x.Id == form.Owner.Id);

View file

@ -0,0 +1,19 @@
@using MoonCore.Abstractions
@using Moonlight.Features.Servers.Entities
@inject Repository<Server> ServerRepository
<a href="/admin/servers">
<StatCard Value="@ServerCount.ToString()" Description="Servers" Icon="bxs-server"></StatCard>
</a>
@code {
private int ServerCount;
protected override async Task OnInitializedAsync()
{
ServerCount = ServerRepository.Get().Count();
}
}

View file

@ -1,5 +1,4 @@
@using Moonlight.Features.Servers.Entities
@using Moonlight.Features.Servers.Models.Abstractions
@using Moonlight.Features.Servers.Models.Enums
@using Moonlight.Features.Servers.Services
@using Moonlight.Features.Servers.UI.Components
@ -11,102 +10,116 @@
@using Moonlight.Features.Servers.UI.UserViews
@using System.Net.Sockets
@using System.Net.WebSockets
@using MoonCore.Exceptions
@using Moonlight.Features.Servers.Configuration
@using MoonCore.Services
@using Moonlight.Core.Services
@inject Repository<Server> ServerRepository
@inject ServerService ServerService
@inject ToastService ToastService
@inject AlertService AlertService
@inject IdentityService IdentityService
@inject ConfigService<ServersConfiguration> ConfigService
@implements IDisposable
<LazyLoader Load="Load" ShowAsCard="true">
<div class="card card-body pb-5 pt-5">
<div class="d-flex justify-content-between">
<div class="d-flex flex-row">
<div class="d-flex flex-column ms-3">
<span class="fw-bold text-gray-900 fs-3">
<div>
<div class="row px-2">
<div class="col-12 col-sm-3">
<span class="fw-bold text-gray-900 fs-3 d-block">
@Server.Name
</span>
<span class="text-gray-500 pt-2 fw-semibold fs-6">
<span class="text-gray-500 pt-2 fw-semibold fs-6 d-block">
@(Server.Image.Name)
</span>
</div>
<div class="vr mx-4"></div>
<div class="d-flex flex-column">
<div class="text-gray-900 fs-4">
@{
var color = ServerUtilsHelper.GetColorFromState(Console.State);
}
<div class="vr p-0 mx-4 d-none d-sm-block"></div>
<hr class="col-sm my-4 d-block d-sm-none"/>
<div class="col-12 col-sm">
<div class="row">
<div class="col">
<div class="text-gray-900 fs-4">
@{
var color = ServerUtilsHelper.GetColorFromState(Console.State);
}
<i class="bx bx-sm bxs-circle text-@(color) @(Console.State != ServerState.Offline ? $"pulse pulse-{color}" : "") align-middle"></i>
<span class="align-middle">
@(Console.State)
<span class="text-muted">(@(Formatter.FormatUptime(DateTime.UtcNow - Console.LastStateChangeTimestamp)))</span>
</span>
</div>
<div class="text-gray-800 pt-3 fw-semibold fs-5 row">
<div class="col-auto">
<span>
<i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">@(Server.Node.Fqdn):@(Server.MainAllocation.Port)</span>
<i class="bx bx-sm bxs-circle text-@(color) @(Console.State != ServerState.Offline ? $"pulse pulse-{color}" : "") align-middle"></i>
<span class="align-middle">
@(Console.State)
<span class="text-muted">(@(Formatter.FormatUptime(DateTime.UtcNow - Console.LastStateChangeTimestamp)))</span>
</span>
</div>
<div class="text-gray-800 pt-3 fw-semibold fs-5 row">
<div class="col-auto">
<span>
<i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">@(Server.Node.Fqdn):@(Server.MainAllocation.Port)</span>
</span>
</div>
@*
<div class="col-auto">
<span>
<i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">188.75.252.37:10324</span>
</span>
</div>
*@
</div>
</div>
<div class="col-auto">
<span>
<i class="bx bx-sm bx-globe align-middle text-info"></i>
<span class="align-middle">188.75.252.37:10324</span>
</span>
<div class="mt-2">
@if (Console.State == ServerState.Offline)
{
<WButton
OnClick="Start"
CssClasses="btn btn-light-success btn-icon me-1 my-1">
<i class="bx bx-sm bx-play"></i>
</WButton>
}
else
{
<button type="button" class="btn btn-light-success btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-play"></i>
</button>
}
@if (Console.State == ServerState.Offline || Console.State == ServerState.Installing)
{
<button class="btn btn-light-warning btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-power-off"></i>
</button>
}
else
{
<WButton
OnClick="Stop"
CssClasses="btn btn-light-warning btn-icon me-1 my-1">
<i class="bx bx-sm bx-power-off"></i>
</WButton>
}
@if (Console.State == ServerState.Offline || Console.State == ServerState.Installing)
{
<button class="btn btn-light-danger btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-bomb"></i>
</button>
}
else
{
<WButton
OnClick="Kill"
CssClasses="btn btn-light-danger btn-icon me-1 my-1">
<i class="bx bx-sm bx-bomb"></i>
</WButton>
}
</div>
</div>
</div>
</div>
</div>
<div>
<div class="mt-2">
@if (Console.State == ServerState.Offline)
{
<WButton
OnClick="Start"
CssClasses="btn btn-light-success btn-icon me-1 my-1">
<i class="bx bx-sm bx-play"></i>
</WButton>
}
else
{
<button type="button" class="btn btn-light-success btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-play"></i>
</button>
}
@if (Console.State == ServerState.Offline || Console.State == ServerState.Installing)
{
<button class="btn btn-light-warning btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-power-off"></i>
</button>
}
else
{
<WButton
OnClick="Stop"
CssClasses="btn btn-light-warning btn-icon me-1 my-1">
<i class="bx bx-sm bx-power-off"></i>
</WButton>
}
@if (Console.State == ServerState.Offline || Console.State == ServerState.Installing)
{
<button class="btn btn-light-danger btn-icon me-1 my-1 disabled" disabled="">
<i class="bx bx-sm bx-bomb"></i>
</button>
}
else
{
<WButton
OnClick="Kill"
CssClasses="btn btn-light-danger btn-icon me-1 my-1">
<i class="bx bx-sm bx-bomb"></i>
</WButton>
}
</div>
</div>
</div>
</div>
@ -211,6 +224,12 @@
.Include(x => x.Owner)
.First(x => x.Id == Id);
if (Server.Owner.Id != IdentityService.CurrentUser.Id && IdentityService.CurrentUser.Permissions < 5000)
{
Server = null!;
return;
}
await lazyLoader.SetText("Establishing a connection to the server");
// Create console wrapper
@ -301,11 +320,39 @@
await InstallTerminal.WriteLine(message);
}
private async Task Start() => await ServerService.Console.SendAction(Server, PowerAction.Start);
private async Task Start() => await SendSignalHandled(PowerAction.Start);
private async Task Stop() => await ServerService.Console.SendAction(Server, PowerAction.Stop);
private async Task Stop() => await SendSignalHandled(PowerAction.Stop);
private async Task Kill() => await ServerService.Console.SendAction(Server, PowerAction.Kill);
private async Task Kill()
{
if (!ConfigService.Get().DisableServerKillWarning)
{
if (!await AlertService.YesNo("Do you really want to kill the server? This can result in data loss or corrupted server files"))
return;
}
await SendSignalHandled(PowerAction.Kill);
}
private async Task SendSignalHandled(PowerAction action)
{
try
{
await ServerService.Console.SendAction(Server, action);
}
catch (DisplayException)
{
throw;
}
catch (Exception e)
{
Logger.Warn($"An error occured while sending power action {action} to server {Server.Id}:");
Logger.Warn(e);
await ToastService.Danger("An error occured while sending power action to server. Check the console for more information");
}
}
public async void Dispose()
{

View file

@ -18,7 +18,7 @@
</div>
<div class="col-md-3 col-12">
@{
var memoryText = $"{Formatter.FormatSize(Status.Hardware.Memory.Total - Status.Hardware.Memory.Available - Status.Hardware.Memory.Free)} / {Formatter.FormatSize(Status.Hardware.Memory.Total)}";
var memoryText = $"{Formatter.FormatSize(Status.Hardware.Memory.Total - (Status.Hardware.Memory.Available + Status.Hardware.Memory.Cached))} / {Formatter.FormatSize(Status.Hardware.Memory.Total)}";
}
<StatCard Value="@memoryText" Description="Memory usage" Icon="bxs-microchip"/>

View file

@ -78,7 +78,11 @@
@if (NodeStats.ContainsKey(context!.Id) && NodeStats[context.Id] != null)
{
var memory = NodeStats[context!.Id]!.Hardware.Memory;
var percent = Math.Round(((float)memory.Total - memory.Available - memory.Free) / memory.Total * 100, 2);
var used = memory.Total - (memory.Available + memory.Cached);
var percent = Math.Round((float) used / memory.Total * 100F, 2);
//Logger.Debug($"Used: {used} Total: {memory.Total} => {percent}% ({Formatter.FormatSize(used)} / {Formatter.FormatSize(memory.Total)})");
<ColoredBar Value="percent"/>
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
@ -53,6 +53,7 @@
<Folder Include="Core\Database\Migrations\" />
<Folder Include="Core\Http\Requests\" />
<Folder Include="Core\Http\Resources\" />
<Folder Include="Core\Implementations\UI\Admin\AdminComponents\" />
<Folder Include="Core\UI\Components\Forms\" />
<Folder Include="Features\Dummy\Configuration\" />
<Folder Include="Features\Dummy\Entities\" />
@ -74,6 +75,7 @@
<Folder Include="Features\FileManager\Http\Requests\" />
<Folder Include="Features\FileManager\Http\Resources\" />
<Folder Include="Features\Servers\Http\Resources\" />
<Folder Include="Features\Servers\Implementations\UI\Admin\" />
<Folder Include="storage\" />
<Folder Include="Styles\" />
<Folder Include="wwwroot\css\" />
@ -89,31 +91,11 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MoonCore" Version="1.2.7" />
<PackageReference Include="MoonCoreUI" Version="1.1.6" />
<PackageReference Include="MoonCore" Version="1.3.3" />
<PackageReference Include="MoonCoreUI" Version="1.1.7" />
<PackageReference Include="Otp.NET" Version="1.3.0" />
<PackageReference Include="QRCoder" Version="1.4.3" />
<PackageReference Include="XtermBlazor" Version="1.10.2" />
<PackageReference Include="Z.Blazor.Diagrams" Version="3.0.2" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="wwwroot\fonts\boxicons.eot" />
<_ContentIncludedByDefault Remove="wwwroot\fonts\boxicons.svg" />
<_ContentIncludedByDefault Remove="wwwroot\fonts\boxicons.ttf" />
<_ContentIncludedByDefault Remove="wwwroot\fonts\boxicons.woff" />
<_ContentIncludedByDefault Remove="wwwroot\fonts\boxicons.woff2" />
<_ContentIncludedByDefault Remove="wwwroot\fonts\Inter.woff2" />
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleEditor.razor" />
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleLinkItem.razor" />
<_ContentIncludedByDefault Remove="Features\ScheduleDesigner\UI\Components\ScheduleNodeItem.razor" />
<_ContentIncludedByDefault Remove="wwwroot\svg\logo.svg" />
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\Editor.razor" />
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileEditor.razor" />
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileManager.razor" />
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileUploader.razor" />
<_ContentIncludedByDefault Remove="Features\FileManager\UI\Components\FileView.razor" />
<_ContentIncludedByDefault Remove="storage\configs\core.json" />
</ItemGroup>
</Project>

View file

@ -17,7 +17,7 @@
<title>Moonlight</title>
<link rel="shortcut icon" href="/img/logo.svg">
<link rel="shortcut icon" href="/api/core/asset/Core/svg/logo.svg">
<!-- Assets (css) -->
@foreach (var asset in FeatureService.PreInitContext.Assets)

View file

@ -6,6 +6,7 @@ using Moonlight.Core.Configuration;
using Moonlight.Core.Database;
using Moonlight.Core.Http.Middleware;
using Moonlight.Core.Services;
using MySqlConnector;
// Create needed storage directories
Directory.CreateDirectory(PathBuilder.Dir("storage"));
@ -74,11 +75,29 @@ await featureService.Load();
var pluginService = new PluginService();
await pluginService.Load();
// Check database migrations
await DatabaseCheckHelper.Check(
new DataContext(configService),
false
);
try
{
// Check database migrations
await DatabaseCheckHelper.Check(
new DataContext(configService),
false
);
}
catch (MySqlException e)
{
if (e.InnerException is EndOfStreamException eosException)
{
if (eosException.Message.Contains("read 4 header bytes"))
{
Logger.Warn("The mysql server appears to be still booting up. Exiting...");
Environment.Exit(1);
return;
}
}
throw;
}
// Add pre constructed services
builder.Services.AddSingleton(featureService);

View file

@ -66,4 +66,5 @@ Distributed under the CC0 1.0 Universal License. See [LICENSE](https://github.co
## Authors
* **Masu Baumgartner** - *Endelon Hosting* - [Masu Baumgartner](https://github.com/Marcel-Baumgartner) - *Moonlights core system & frontend and basiclly any other parts of moonlight*
* **Masu Baumgartner** - [Masu Baumgartner](https://github.com/Masu-Baumgartner) - *Moonlights core system & frontend and basiclly any other part of moonlight*
* **Moritz Deiaco** - [Moritz Deiaco](https://github.com/Moritz-Deiaco) - *Moonlight Core and UI*