Merge pull request #209 from Moonlight-Panel/NewVisualConfigEditor

Added a new visual config editor
This commit is contained in:
Marcel Baumgartner 2023-07-06 16:46:29 +02:00 committed by GitHub
commit d1c9009e9f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 442 additions and 42 deletions

View file

@ -1,19 +1,25 @@
namespace Moonlight.App.Configuration;
using System.ComponentModel;
using Moonlight.App.Helpers;
namespace Moonlight.App.Configuration;
using System;
using Newtonsoft.Json;
public class ConfigV1
{
[JsonProperty("Moonlight")] public MoonlightData Moonlight { get; set; } = new();
[JsonProperty("Moonlight")]
public MoonlightData Moonlight { get; set; } = new();
public class MoonlightData
{
[JsonProperty("AppUrl")] public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
[JsonProperty("AppUrl")]
[Description("The url moonlight is accesible with from the internet")]
public string AppUrl { get; set; } = "http://your-moonlight-url-without-slash";
[JsonProperty("Database")] public DatabaseData Database { get; set; } = new();
[JsonProperty("DiscordBotApi")] public DiscordBotData DiscordBotApi { get; set; } = new();
[JsonProperty("DiscordBotApi")] public DiscordBotApiData DiscordBotApi { get; set; } = new();
[JsonProperty("DiscordBot")] public DiscordBotData DiscordBot { get; set; } = new();
@ -47,17 +53,29 @@ public class ConfigV1
public class CleanupData
{
[JsonProperty("Cpu")] public long Cpu { get; set; } = 90;
[JsonProperty("Cpu")]
[Description("The maximum amount of cpu usage in percent a node is allowed to use before the cleanup starts")]
public long Cpu { get; set; } = 90;
[JsonProperty("Memory")] public long Memory { get; set; } = 8192;
[JsonProperty("Memory")]
[Description("The minumum amount of memory in megabytes avaliable before the cleanup starts")]
public long Memory { get; set; } = 8192;
[JsonProperty("Wait")] public long Wait { get; set; } = 15;
[JsonProperty("Wait")]
[Description("The delay between every cleanup check in minutes")]
public long Wait { get; set; } = 15;
[JsonProperty("Uptime")] public long Uptime { get; set; } = 6;
[JsonProperty("Uptime")]
[Description("The maximum uptime of any server in hours before it the server restarted by the cleanup system")]
public long Uptime { get; set; } = 6;
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Enable")]
[Description("The cleanup system provides a fair way for stopping unused servers and staying stable even with overallocation. A detailed explanation: docs.endelon-hosting.de/erklaerungen/cleanup")]
public bool Enable { get; set; } = false;
[JsonProperty("MinUptime")] public long MinUptime { get; set; } = 10;
[JsonProperty("MinUptime")]
[Description("The minumum uptime of a server in minutes to prevent stopping servers which just started")]
public long MinUptime { get; set; } = 10;
}
public class DatabaseData
@ -66,38 +84,76 @@ public class ConfigV1
[JsonProperty("Host")] public string Host { get; set; } = "your.database.host";
[JsonProperty("Password")] public string Password { get; set; } = "secret";
[JsonProperty("Password")]
[Blur]
public string Password { get; set; } = "secret";
[JsonProperty("Port")] public long Port { get; set; } = 3306;
[JsonProperty("Username")] public string Username { get; set; } = "moonlight_user";
}
public class DiscordBotApiData
{
[JsonProperty("Enable")]
[Description("Enable the discord bot api. Currently only DatBot is using this api")]
public bool Enable { get; set; } = false;
[JsonProperty("Token")]
[Description("Specify the token the api client needs to provide")]
[Blur]
public string Token { get; set; } = Guid.NewGuid().ToString();
}
public class DiscordBotData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Enable")]
[Description("The discord bot can be used to allow customers to manage their servers via discord")]
public bool Enable { get; set; } = false;
[JsonProperty("Token")] public string Token { get; set; } = "discord token here";
[JsonProperty("Token")]
[Description("Your discord bot token goes here")]
[Blur]
public string Token { get; set; } = "discord token here";
[JsonProperty("PowerActions")] public bool PowerActions { get; set; } = false;
[JsonProperty("SendCommands")] public bool SendCommands { get; set; } = false;
[JsonProperty("PowerActions")]
[Description("Enable actions like starting and stopping servers")]
public bool PowerActions { get; set; } = false;
[JsonProperty("SendCommands")]
[Description("Allow users to send commands to their servers")]
public bool SendCommands { get; set; } = false;
}
public class DiscordNotificationsData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Enable")]
[Description("The discord notification system sends you a message everytime a event like a new support chat message is triggered with usefull data describing the event")]
public bool Enable { get; set; } = false;
[JsonProperty("WebHook")] public string WebHook { get; set; } = "http://your-discord-webhook-url";
[JsonProperty("WebHook")]
[Description("The discord webhook the notifications are being sent to")]
[Blur]
public string WebHook { get; set; } = "http://your-discord-webhook-url";
}
public class DomainsData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("AccountId")] public string AccountId { get; set; } = "cloudflare acc id";
[JsonProperty("Enable")]
[Description("This enables the domain system")]
public bool Enable { get; set; } = false;
[JsonProperty("Email")] public string Email { get; set; } = "cloudflare@acc.email";
[JsonProperty("AccountId")]
[Description("This option specifies the cloudflare account id")]
public string AccountId { get; set; } = "cloudflare acc id";
[JsonProperty("Key")] public string Key { get; set; } = "secret";
[JsonProperty("Email")]
[Description("This specifies the cloudflare email to use for communicating with the cloudflare api")]
public string Email { get; set; } = "cloudflare@acc.email";
[JsonProperty("Key")]
[Description("Your cloudflare api key goes here")]
[Blur]
public string Key { get; set; } = "secret";
}
public class HtmlData
@ -107,13 +163,21 @@ public class ConfigV1
public class HeadersData
{
[JsonProperty("Color")] public string Color { get; set; } = "#4b27e8";
[JsonProperty("Color")]
[Description("This specifies the color of the embed generated by platforms like discord when someone posts a link to your moonlight instance")]
public string Color { get; set; } = "#4b27e8";
[JsonProperty("Description")] public string Description { get; set; } = "the next generation hosting panel";
[JsonProperty("Description")]
[Description("This specifies the description text of the embed generated by platforms like discord when someone posts a link to your moonlight instance and can also help google to index your moonlight instance correctly")]
public string Description { get; set; } = "the next generation hosting panel";
[JsonProperty("Keywords")] public string Keywords { get; set; } = "moonlight";
[JsonProperty("Keywords")]
[Description("To help search engines like google to index your moonlight instance correctly you can specify keywords seperated by a comma here")]
public string Keywords { get; set; } = "moonlight";
[JsonProperty("Title")] public string Title { get; set; } = "Moonlight - endelon.link";
[JsonProperty("Title")]
[Description("This specifies the title of the embed generated by platforms like discord when someone posts a link to your moonlight instance")]
public string Title { get; set; } = "Moonlight - endelon.link";
}
public class MailData
@ -122,7 +186,9 @@ public class ConfigV1
[JsonProperty("Server")] public string Server { get; set; } = "your.mail.host";
[JsonProperty("Password")] public string Password { get; set; } = "secret";
[JsonProperty("Password")]
[Blur]
public string Password { get; set; } = "secret";
[JsonProperty("Port")] public int Port { get; set; } = 465;
@ -142,9 +208,13 @@ public class ConfigV1
public class OAuth2Data
{
[JsonProperty("OverrideUrl")] public string OverrideUrl { get; set; } = "https://only-for-development.cases";
[JsonProperty("OverrideUrl")]
[Description("This overrides the redirect url which would be typicaly the app url")]
public string OverrideUrl { get; set; } = "https://only-for-development.cases";
[JsonProperty("EnableOverrideUrl")] public bool EnableOverrideUrl { get; set; } = false;
[JsonProperty("EnableOverrideUrl")]
[Description("This enables the url override")]
public bool EnableOverrideUrl { get; set; } = false;
[JsonProperty("Providers")]
public OAuth2ProviderData[] Providers { get; set; } = Array.Empty<OAuth2ProviderData>();
@ -156,41 +226,65 @@ public class ConfigV1
[JsonProperty("ClientId")] public string ClientId { get; set; }
[JsonProperty("ClientSecret")] public string ClientSecret { get; set; }
[JsonProperty("ClientSecret")]
[Blur]
public string ClientSecret { get; set; }
}
public class RatingData
{
[JsonProperty("Enabled")] public bool Enabled { get; set; } = false;
[JsonProperty("Enabled")]
[Description("The rating systems shows a user who is registered longer than the set amout of days a popup to rate this platform if he hasnt rated it before")]
public bool Enabled { get; set; } = false;
[JsonProperty("Url")] public string Url { get; set; } = "https://link-to-google-or-smth";
[JsonProperty("Url")]
[Description("This is the url a user who rated above a set limit is shown to rate you again. Its recommended to put your google or trustpilot rate link here")]
public string Url { get; set; } = "https://link-to-google-or-smth";
[JsonProperty("MinRating")] public int MinRating { get; set; } = 4;
[JsonProperty("MinRating")]
[Description("The minimum star count on the rating ranging from 1 to 5")]
public int MinRating { get; set; } = 4;
[JsonProperty("DaysSince")] public int DaysSince { get; set; } = 5;
[JsonProperty("DaysSince")]
[Description("The days a user has to be registered to even be able to get this popup")]
public int DaysSince { get; set; } = 5;
}
public class SecurityData
{
[JsonProperty("Token")] public string Token { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("Token")]
[Description("This is the moonlight app token. It is used to encrypt and decrypt data and validte tokens and sessions")]
[Blur]
public string Token { get; set; } = Guid.NewGuid().ToString();
[JsonProperty("ReCaptcha")] public ReCaptchaData ReCaptcha { get; set; } = new();
}
public class ReCaptchaData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Enable")]
[Description("Enables repatcha at places like the register page. For information how to get your recaptcha credentails go to google.com/recaptcha/about/")]
public bool Enable { get; set; } = false;
[JsonProperty("SiteKey")] public string SiteKey { get; set; } = "recaptcha site key here";
[JsonProperty("SiteKey")]
[Blur]
public string SiteKey { get; set; } = "recaptcha site key here";
[JsonProperty("SecretKey")] public string SecretKey { get; set; } = "recaptcha secret here";
[JsonProperty("SecretKey")]
[Blur]
public string SecretKey { get; set; } = "recaptcha secret here";
}
public class SentryData
{
[JsonProperty("Enable")] public bool Enable { get; set; } = false;
[JsonProperty("Enable")]
[Description("Sentry is a way to monitor application crashes and performance issues in real time. Enable this option only if you set a sentry dsn")]
public bool Enable { get; set; } = false;
[JsonProperty("Dsn")] public string Dsn { get; set; } = "http://your-sentry-url-here";
[JsonProperty("Dsn")]
[Description("The dsn is the key moonlight needs to communicate with your sentry instance")]
[Blur]
public string Dsn { get; set; } = "http://your-sentry-url-here";
}
public class SmartDeployData

View file

@ -0,0 +1,6 @@
namespace Moonlight.App.Helpers;
public class BlurAttribute : Attribute
{
}

View file

@ -1,9 +1,36 @@
using Moonlight.App.Services;
using System.Text;
using Moonlight.App.Services;
namespace Moonlight.App.Helpers;
public static class Formatter
{
public static string ReplaceEnd(string input, string substringToReplace, string newSubstring)
{
int lastIndexOfSubstring = input.LastIndexOf(substringToReplace);
if (lastIndexOfSubstring >= 0)
{
input = input.Remove(lastIndexOfSubstring, substringToReplace.Length).Insert(lastIndexOfSubstring, newSubstring);
}
return input;
}
public static string ConvertCamelCaseToSpaces(string input)
{
StringBuilder output = new StringBuilder();
foreach (char c in input)
{
if (char.IsUpper(c))
{
output.Append(' ');
}
output.Append(c);
}
return output.ToString().Trim();
}
public static string FormatUptime(double uptime)
{
TimeSpan t = TimeSpan.FromMilliseconds(uptime);

View file

@ -0,0 +1,51 @@
using System.Reflection;
namespace Moonlight.App.Helpers;
public class PropBinder
{
private PropertyInfo PropertyInfo;
private object DataObject;
public PropBinder(PropertyInfo propertyInfo, object dataObject)
{
PropertyInfo = propertyInfo;
DataObject = dataObject;
}
public string StringValue
{
get => (string)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public int IntValue
{
get => (int)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public long LongValue
{
get => (long)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public bool BoolValue
{
get => (bool)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public DateTime DateTimeValue
{
get => (DateTime)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
public double DoubleValue
{
get => (double)PropertyInfo.GetValue(DataObject)!;
set => PropertyInfo.SetValue(DataObject, value);
}
}

View file

@ -51,7 +51,27 @@ public class ConfigService
File.ReadAllText(path)
) ?? new ConfigV1();
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration));
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented));
}
public void Save(ConfigV1 configV1)
{
Configuration = configV1;
Save();
}
public void Save()
{
var path = PathBuilder.File("storage", "configs", "config.json");
if (!File.Exists(path))
{
File.WriteAllText(path, "{}");
}
File.WriteAllText(path, JsonConvert.SerializeObject(Configuration, Formatting.Indented));
Reload();
}
public ConfigV1 Get()

View file

@ -0,0 +1,65 @@
@using System.Reflection
@using System.Collections
@using Moonlight.App.Helpers
<div class="accordion my-3" id="configSetting@(Model.GetHashCode())">
<div class="accordion-item">
<h2 class="accordion-header" id="configSetting-header@(Model.GetHashCode())">
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#configSetting-body@(Model.GetHashCode())" aria-expanded="false" aria-controls="configSetting-body@(Model.GetHashCode())">
@{
var name = Formatter.ReplaceEnd(Model.GetType().Name, "Data", "");
name = Formatter.ConvertCamelCaseToSpaces(name);
}
@(name)
</button>
</h2>
<div id="configSetting-body@(Model.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="configSetting-header@(Model.GetHashCode())" data-bs-parent="#configSetting">
<div class="accordion-body">
@foreach (var property in Model.GetType().GetProperties())
{
@BindAndRenderProperty(property)
}
</div>
</div>
</div>
</div>
@code
{
[Parameter]
public object Model { get; set; }
private RenderFragment BindAndRenderProperty(PropertyInfo property)
{
if (property.PropertyType.IsClass && !property.PropertyType.IsPrimitive && !typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
return @<SmartFormClass Model="@property.GetValue(Model)"/>;
// If the property is a subclass, serialize and generate form for it
/*
foreach (var subProperty in property.PropertyType.GetProperties())
{
return BindAndRenderProperty(subProperty);
}*/
}
else if (property.PropertyType == typeof(int) || property.PropertyType == typeof(string) || property.PropertyType == typeof(bool) || property.PropertyType == typeof(decimal) || property.PropertyType == typeof(long))
{
return @<SmartFormProperty Model="Model" PropertyInfo="property"/>;
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
// If the property is a collection, generate form for each element
var collection = property.GetValue(Model) as IEnumerable;
if (collection != null)
{
foreach (var element in collection)
{
}
}
}
// Additional property types could be handled here (e.g., DateTime, int, etc.)
return @<div></div>;
}
}

View file

@ -0,0 +1,79 @@
@using System.Reflection
@using Moonlight.App.Helpers
@using System.ComponentModel
<label class="form-label" for="@PropertyInfo.Name">
@(Formatter.ConvertCamelCaseToSpaces(PropertyInfo.Name))
</label>
@{
//TODO: Tidy up this code
var attrs = PropertyInfo.GetCustomAttributes(true);
var descAttr = attrs
.FirstOrDefault(x => x.GetType() == typeof(DescriptionAttribute));
var blurBool = attrs.Any(x => x.GetType() == typeof(BlurAttribute));
var blur = blurBool ? "blur-unless-hover" : "";
}
@if (descAttr != null)
{
var a = descAttr as DescriptionAttribute;
<div class="form-text fs-5 mb-2 mt-0">
@(a.Description)
</div>
}
<div class="input-group mb-5">
@if (PropertyInfo.PropertyType == typeof(string))
{
var binder = new PropBinder(PropertyInfo, Model!);
<div class="@(blur) w-100">
<InputText id="@PropertyInfo.Name" @bind-Value="binder.StringValue" class="form-control"/>
</div>
}
else if (PropertyInfo.PropertyType == typeof(int))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputNumber id="@PropertyInfo.Name" @bind-Value="binder.IntValue" class="form-control"/>
}
else if (PropertyInfo.PropertyType == typeof(long))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputNumber id="@PropertyInfo.Name" @bind-Value="binder.LongValue" class="form-control"/>
}
else if (PropertyInfo.PropertyType == typeof(bool))
{
var binder = new PropBinder(PropertyInfo, Model!);
<div class="form-check">
<InputCheckbox id="@PropertyInfo.Name" @bind-Value="binder.BoolValue" class="form-check-input"/>
</div>
}
else if (PropertyInfo.PropertyType == typeof(DateTime))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputDate id="@PropertyInfo.Name" @bind-Value="binder.DateTimeValue" class="form-control"/>
}
else if (PropertyInfo.PropertyType == typeof(decimal))
{
var binder = new PropBinder(PropertyInfo, Model!);
<InputNumber id="@PropertyInfo.Name" step="0.01" @bind-Value="binder.DoubleValue" class="form-control"/>
}
</div>
@code
{
[Parameter]
public PropertyInfo PropertyInfo { get; set; }
[Parameter]
public object Model { get; set; }
}

View file

@ -39,6 +39,11 @@
<TL>News</TL>
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 8 ? "active" : "")" href="/admin/system/configuration">
<TL>Configuration</TL>
</a>
</li>
</ul>
</div>
</div>

View file

@ -0,0 +1,53 @@
@page "/admin/system/configuration"
@using Moonlight.App.Services
@using Moonlight.Shared.Components.Navigations
@using Moonlight.App.Configuration
@using Moonlight.App.Services.Interop
@inject ConfigService ConfigService
@inject ToastService ToastService
@inject SmartTranslateService SmartTranslateService
<OnlyAdmin>
<AdminSystemNavigation Index="8"/>
<LazyLoader Load="Load">
<div class="card">
<SmartForm Model="Config" OnValidSubmit="OnSubmit">
<div class="card-body">
<SmartFormClass Model="Config"/>
</div>
<div class="card-footer">
<div class="text-end">
<button type="submit" class="btn btn-success">
<TL>Save</TL>
</button>
</div>
</div>
</SmartForm>
</div>
</LazyLoader>
</OnlyAdmin>
@code
{
private ConfigV1 Config;
private Task Load(LazyLoader lazyLoader)
{
Config = ConfigService.Get();
return Task.CompletedTask;
}
private async Task OnSubmit()
{
ConfigService.Save(Config);
await ToastService.Success(
SmartTranslateService.Translate(
"Successfully saved and reloaded configuration. Some changes may take affect after a restart of moonlight"
)
);
}
}