Started implementing image editor and parse config editor
This commit is contained in:
parent
6fd1336f1c
commit
99a7d7bd73
13 changed files with 1859 additions and 1 deletions
1040
Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.Designer.cs
generated
Normal file
1040
Moonlight/Core/Database/Migrations/20240127164420_FixedMissingPropertyInServerImage.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,48 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.Core.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class FixedMissingPropertyInServerImage : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ServerImageId",
|
||||
table: "ServerImageVariables",
|
||||
type: "INTEGER",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ServerImageVariables_ServerImageId",
|
||||
table: "ServerImageVariables",
|
||||
column: "ServerImageId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_ServerImageVariables_ServerImages_ServerImageId",
|
||||
table: "ServerImageVariables",
|
||||
column: "ServerImageId",
|
||||
principalTable: "ServerImages",
|
||||
principalColumn: "Id");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_ServerImageVariables_ServerImages_ServerImageId",
|
||||
table: "ServerImageVariables");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_ServerImageVariables_ServerImageId",
|
||||
table: "ServerImageVariables");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ServerImageId",
|
||||
table: "ServerImageVariables");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -353,8 +353,13 @@ namespace Moonlight.Core.Database.Migrations
|
|||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("ServerImageId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ServerImageId");
|
||||
|
||||
b.ToTable("ServerImageVariables");
|
||||
});
|
||||
|
||||
|
@ -856,6 +861,13 @@ namespace Moonlight.Core.Database.Migrations
|
|||
.HasForeignKey("ServerImageId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImageVariable", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.Features.Servers.Entities.ServerImage", null)
|
||||
.WithMany("Variables")
|
||||
.HasForeignKey("ServerImageId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerVariable", b =>
|
||||
{
|
||||
b.HasOne("Moonlight.Features.Servers.Entities.Server", null)
|
||||
|
@ -1001,6 +1013,8 @@ namespace Moonlight.Core.Database.Migrations
|
|||
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerImage", b =>
|
||||
{
|
||||
b.Navigation("DockerImages");
|
||||
|
||||
b.Navigation("Variables");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Moonlight.Features.Servers.Entities.ServerNode", b =>
|
||||
|
|
33
Moonlight/Core/Helpers/ValidatorHelper.cs
Normal file
33
Moonlight/Core/Helpers/ValidatorHelper.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Moonlight.Core.Exceptions;
|
||||
|
||||
namespace Moonlight.Core.Helpers;
|
||||
|
||||
public class ValidatorHelper
|
||||
{
|
||||
public static Task Validate(object objectToValidate)
|
||||
{
|
||||
var context = new ValidationContext(objectToValidate, null, null);
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
var isValid = Validator.TryValidateObject(objectToValidate, context, results, true);
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
var errorMsg = "Unknown form error";
|
||||
|
||||
if (results.Any())
|
||||
errorMsg = results.First().ErrorMessage ?? errorMsg;
|
||||
|
||||
throw new DisplayException(errorMsg);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static async Task ValidateRange(IEnumerable<object> objectToValidate)
|
||||
{
|
||||
foreach (var o in objectToValidate)
|
||||
await Validate(o);
|
||||
}
|
||||
}
|
244
Moonlight/Core/UI/Components/Forms/AutoListCrud.razor
Normal file
244
Moonlight/Core/UI/Components/Forms/AutoListCrud.razor
Normal file
|
@ -0,0 +1,244 @@
|
|||
@using BlazorTable
|
||||
@using System.Linq.Expressions
|
||||
@using Mappy.Net
|
||||
@using Moonlight.Core.Services.Interop
|
||||
|
||||
@typeparam TItem where TItem : class
|
||||
@typeparam TRootItem where TRootItem : class
|
||||
@typeparam TCreateForm
|
||||
@typeparam TUpdateForm
|
||||
|
||||
@inject ToastService ToastService
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">@(Title)</h3>
|
||||
<div class="card-toolbar">
|
||||
@Toolbar
|
||||
<button @onclick="StartCreate" class="btn btn-icon btn-success ms-3">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="LoadItems">
|
||||
<Table TableItem="TItem"
|
||||
Items="Items"
|
||||
PageSize="50"
|
||||
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||
TableHeadClass="fw-bold text-muted">
|
||||
@View
|
||||
<Column TableItem="TItem" Field="IdExpression" Title="" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="text-end">
|
||||
<div class="btn-group">
|
||||
<button @onclick="() => StartUpdate(context)" class="btn btn-icon btn-warning">
|
||||
<i class="bx bx-sm bx-slider"></i>
|
||||
</button>
|
||||
<button @onclick="() => StartDelete(context)" class="btn btn-icon btn-danger">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Template>
|
||||
</Column>
|
||||
</Table>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmartModal @ref="CreateModal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Create</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="CreateForm" OnValidSubmit="FinishCreate">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<AutoForm Columns="@(CreateForm.GetType().GetProperties().Length > 1 ? 6 : 12)" Model="CreateForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="UpdateModal" CssClasses="modal-dialog-centered modal-lg">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Update</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<SmartForm Model="UpdateForm" OnValidSubmit="FinishUpdate">
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<AutoForm Columns="@(UpdateForm.GetType().GetProperties().Length > 1 ? 6 : 12)" Model="UpdateForm"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<button type="submit" class="btn btn-primary">Save changes</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</SmartModal>
|
||||
|
||||
<SmartModal @ref="DeleteModal" CssClasses="modal-dialog-centered">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Do you want to delete this item?</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="text-gray-400 fs-5 fw-semibold">
|
||||
This action cannot be undone. The data will be deleted and cannot be restored
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer p-3">
|
||||
<div class="btn-group w-100">
|
||||
<WButton OnClick="FinishDelete" Text="Delete" CssClasses="btn btn-danger w-50 me-3"/>
|
||||
<button class="btn btn-secondary w-50" data-bs-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</SmartModal>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public string Title { get; set; } = "";
|
||||
|
||||
[Parameter]
|
||||
public TRootItem RootItem { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TRootItem, IList<TItem>> Field { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment View { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment Toolbar { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateAdd { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateUpdate { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateDelete { get; set; }
|
||||
|
||||
private TItem[] Items;
|
||||
private TCreateForm CreateForm;
|
||||
private TUpdateForm UpdateForm;
|
||||
private TItem ItemToUpdate;
|
||||
private TItem ItemToDelete;
|
||||
|
||||
private SmartModal CreateModal;
|
||||
private SmartModal UpdateModal;
|
||||
private SmartModal DeleteModal;
|
||||
|
||||
private Expression<Func<TItem, object>> IdExpression;
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
if (Field == null)
|
||||
throw new ArgumentNullException(nameof(Field));
|
||||
|
||||
CreateForm = Activator.CreateInstance<TCreateForm>()!;
|
||||
UpdateForm = Activator.CreateInstance<TUpdateForm>()!;
|
||||
|
||||
IdExpression = CreateExpression();
|
||||
}
|
||||
|
||||
public async Task Reload() => await LazyLoader.Reload();
|
||||
|
||||
private Task LoadItems(LazyLoader _)
|
||||
{
|
||||
Items = Field.Invoke(RootItem).ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task StartUpdate(TItem item)
|
||||
{
|
||||
UpdateForm = Mapper.Map<TUpdateForm>(item);
|
||||
ItemToUpdate = item;
|
||||
await UpdateModal.Show();
|
||||
}
|
||||
|
||||
private async Task FinishUpdate()
|
||||
{
|
||||
var item = Mapper.Map(ItemToUpdate, UpdateForm!);
|
||||
|
||||
if (ValidateUpdate != null) // Optional additional validation
|
||||
await ValidateUpdate.Invoke(item);
|
||||
|
||||
//ItemRepository.Update(item);
|
||||
|
||||
// Reset
|
||||
await UpdateModal.Hide();
|
||||
await LazyLoader.Reload();
|
||||
await ToastService.Success("Successfully updated item");
|
||||
}
|
||||
|
||||
private async Task StartCreate()
|
||||
{
|
||||
CreateForm = Activator.CreateInstance<TCreateForm>()!;
|
||||
await CreateModal.Show();
|
||||
}
|
||||
|
||||
private async Task FinishCreate()
|
||||
{
|
||||
var item = Mapper.Map<TItem>(CreateForm!);
|
||||
|
||||
if (ValidateAdd != null) // Optional additional validation
|
||||
await ValidateAdd.Invoke(item);
|
||||
|
||||
Field.Invoke(RootItem).Add(item);
|
||||
|
||||
// Reset
|
||||
await CreateModal.Hide();
|
||||
await LazyLoader.Reload();
|
||||
await ToastService.Success("Successfully added item");
|
||||
}
|
||||
|
||||
private async Task StartDelete(TItem item)
|
||||
{
|
||||
ItemToDelete = item;
|
||||
await DeleteModal.Show();
|
||||
}
|
||||
|
||||
private async Task FinishDelete()
|
||||
{
|
||||
if (ValidateDelete != null) // Optional additional validation
|
||||
await ValidateDelete.Invoke(ItemToDelete);
|
||||
|
||||
Field.Invoke(RootItem).Remove(ItemToDelete);
|
||||
|
||||
// Reset
|
||||
await DeleteModal.Hide();
|
||||
await LazyLoader.Reload();
|
||||
await ToastService.Success("Successfully deleted item");
|
||||
}
|
||||
|
||||
private Expression<Func<TItem, object>> CreateExpression()
|
||||
{
|
||||
// Parameter expression for the input object
|
||||
var inputParam = Expression.Parameter(typeof(TItem), "input");
|
||||
|
||||
// Convert the input object to the actual model type (MyModel in this example)
|
||||
var castedInput = Expression.Convert(inputParam, typeof(TItem));
|
||||
|
||||
// Create a property access expression using the property name
|
||||
var propertyAccess = Expression.Property(castedInput, "Id");
|
||||
|
||||
// Convert the property value to an object
|
||||
var castedResult = Expression.Convert(propertyAccess, typeof(object));
|
||||
|
||||
// Create a lambda expression
|
||||
var lambda = Expression.Lambda<Func<TItem, object>>(castedResult, inputParam);
|
||||
|
||||
return lambda;
|
||||
}
|
||||
}
|
|
@ -19,7 +19,7 @@ public class ServerImage
|
|||
public string? DonateUrl { get; set; }
|
||||
public string? UpdateUrl { get; set; }
|
||||
|
||||
public List<ServerImageVariable> Variables = new();
|
||||
public List<ServerImageVariable> Variables { get; set; } = new();
|
||||
|
||||
public int DefaultDockerImageIndex { get; set; } = 0;
|
||||
public List<ServerDockerImage> DockerImages { get; set; }
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Features.Servers.Models.Forms.Admin.Images;
|
||||
|
||||
public class ParseConfigForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to specify a type in a parse configuration")]
|
||||
public string Type { get; set; } = "";
|
||||
|
||||
[Required(ErrorMessage = "You need to specify a file in a parse configuration")]
|
||||
public string File { get; set; } = "";
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Moonlight.Features.Servers.Models.Forms.Admin.Images;
|
||||
|
||||
public class ParseConfigOptionForm
|
||||
{
|
||||
[Required(ErrorMessage = "You need to specify the key of an option")]
|
||||
public string Key { get; set; } = "";
|
||||
public string Value { get; set; } = "";
|
||||
}
|
8
Moonlight/Features/Servers/Models/ServerParseConfig.cs
Normal file
8
Moonlight/Features/Servers/Models/ServerParseConfig.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Moonlight.Features.Servers.Models;
|
||||
|
||||
public class ServerParseConfig
|
||||
{
|
||||
public string Type { get; set; } = "";
|
||||
public string File { get; set; } = "";
|
||||
public Dictionary<string, string> Configuration { get; set; } = new();
|
||||
}
|
154
Moonlight/Features/Servers/UI/Components/ImageEditor.razor
Normal file
154
Moonlight/Features/Servers/UI/Components/ImageEditor.razor
Normal file
|
@ -0,0 +1,154 @@
|
|||
<SmartForm Model="Form" OnValidSubmit="AddImage">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">General</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-5">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Name</label>
|
||||
<input @bind="Form.Name" type="text" class="form-control form-control-solid" placeholder="Name of your image">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Author</label>
|
||||
<input @bind="Form.Author" type="text" class="form-control form-control-solid" placeholder="Name of the image author">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Donate URL</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
(Optional) URL to link donation pages for the author
|
||||
</div>
|
||||
<input @bind="Form.DonateUrl" type="text" class="form-control form-control-solid">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Update URL</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
(Optional) URL to enable auto updates on images. This link needs to be a direct download link to a json file
|
||||
</div>
|
||||
<input @bind="Form.UpdateUrl" type="text" class="form-control form-control-solid">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Installation</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Install docker image</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
This specifies the docker image to use for the script execution
|
||||
</div>
|
||||
<input @bind="Form.InstallDockerImage" type="text" class="form-control form-control-solid">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Install shell</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
This is the shell to pass the install script to
|
||||
</div>
|
||||
<input @bind="Form.InstallShell" type="text" class="form-control form-control-solid">
|
||||
</div>
|
||||
</div>
|
||||
@* TODO: Add vscode editor or similar *@
|
||||
<label class="form-label">Install script</label>
|
||||
<textarea @bind="Form.InstallScript" class="form-control form-control-solid"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Startup, Control & Allocations</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label class="form-label">Startup command</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
This command gets passed to the container of the image to execute. Server variables can be used here
|
||||
</div>
|
||||
<input @bind="Form.StartupCommand" type="text" class="form-control form-control-solid"/>
|
||||
<div class="row g-5 mt-5">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Stop command</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
This command will get written into the input stream of the server process when the server should get stopped
|
||||
</div>
|
||||
<input @bind="Form.StopCommand" type="text" class="form-control form-control-solid">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Online detection</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
The regex string you specify here will be used in order to detect if a server is up and running
|
||||
</div>
|
||||
<input @bind="Form.OnlineDetection" type="text" class="form-control form-control-solid">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Allocations Amount</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
The allocations (aka. ports) a image needs in order to be created
|
||||
</div>
|
||||
<input @bind="Form.AllocationsNeeded" type="number" class="form-control form-control-solid">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Docker images</span>
|
||||
<div class="card-toolbar">
|
||||
<button @onclick="AddDockerImage" type="button" class="btn btn-icon btn-success">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-5">
|
||||
@foreach (var dockerImage in DockerImages)
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input @bind="dockerImage.Name" type="text" class="form-control form-control-solid" placeholder="moonlightpanel/images:minecraft17">
|
||||
<WButton OnClick="() => RemoveDockerImage(dockerImage)" CssClasses="btn btn-danger">Remove</WButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-5">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Variables</span>
|
||||
<div class="card-toolbar">
|
||||
<button @onclick="AddVariable" type="button" class="btn btn-icon btn-success">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-5">
|
||||
@foreach (var variable in Variables)
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input @bind="variable.Key" type="text" class="form-control form-control-solid" placeholder="Key">
|
||||
<input @bind="variable.Value" type="text" class="form-control form-control-solid" placeholder="Default value">
|
||||
<WButton OnClick="() => RemoveVariable(variable)" CssClasses="btn btn-danger">Remove</WButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5">
|
||||
<ParseConfigEditor @ref="ParseConfigEditor"/>
|
||||
</div>
|
||||
<div class="card mt-5">
|
||||
<div class="card-body text-end">
|
||||
<button class="btn btn-success" type="submit">Save changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</SmartForm>
|
||||
|
||||
@code
|
||||
{
|
||||
|
||||
}
|
156
Moonlight/Features/Servers/UI/Components/ParseConfigEditor.razor
Normal file
156
Moonlight/Features/Servers/UI/Components/ParseConfigEditor.razor
Normal file
|
@ -0,0 +1,156 @@
|
|||
@using Newtonsoft.Json
|
||||
@using Mappy.Net
|
||||
@using Moonlight.Core.Helpers
|
||||
@using Moonlight.Features.Servers.Models
|
||||
@using Moonlight.Features.Servers.Models.Forms.Admin.Images
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Parse configurations</span>
|
||||
<div class="card-toolbar">
|
||||
<button @onclick="AddConfig" type="button" class="btn btn-icon btn-success">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@foreach (var config in Configs)
|
||||
{
|
||||
<div class="accordion" id="accordionPc">
|
||||
<div class="accordion-item mb-3">
|
||||
<div class="accordion-header" id="apch@(config.GetHashCode())">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#apcb@(config.GetHashCode())" aria-expanded="false" aria-controls="apcb@(config.GetHashCode())">
|
||||
<span class="h5">@(string.IsNullOrEmpty(config.Key.File) ? "File path is missing" : config.Key.File)</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="apcb@(config.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="apch@(config.GetHashCode())" data-bs-parent="#accordionPc">
|
||||
<div class="accordion-body">
|
||||
<div class="row g-5">
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">File path</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
A relative path from the servers main directory to the file you want to modify
|
||||
</div>
|
||||
<input @bind="config.Key.File" type="text" class="form-control" placeholder="e.g. configs/paper-global.yml">
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<label class="form-label">Type</label>
|
||||
<div class="form-text fs-5 mb-2 mt-0">
|
||||
This specifies the type of parser to use. e.g. "properties" or "file"
|
||||
</div>
|
||||
<input @bind="config.Key.Type" type="text" class="form-control" placeholder="properties">
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="text-end">
|
||||
<WButton OnClick="() => RemoveConfig(config.Key)" CssClasses="btn btn-danger">Remove</WButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mt-3">
|
||||
<div class="card-header border-bottom-0">
|
||||
<div class="card-title"></div>
|
||||
<div class="card-toolbar">
|
||||
<button @onclick="() => AddOption(config.Key)" type="button" class="btn btn-icon btn-success">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body pt-0">
|
||||
<div class="row g-4">
|
||||
@foreach (var option in config.Value)
|
||||
{
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
<input @bind="option.Key" type="text" class="form-control" placeholder="Key">
|
||||
<input @bind="option.Value" type="text" class="form-control" placeholder="Value (Variables with {{VARIABLE}})">
|
||||
<WButton OnClick="() => RemoveOption(config.Key, option)" CssClasses="btn btn-danger">Remove</WButton>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
public string InitialContent { get; set; } = "";
|
||||
|
||||
private Dictionary<ParseConfigForm, List<ParseConfigOptionForm>> Configs = new();
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender && !string.IsNullOrEmpty(InitialContent))
|
||||
await Set(InitialContent);
|
||||
}
|
||||
|
||||
public async Task Set(string content)
|
||||
{
|
||||
Configs.Clear();
|
||||
|
||||
var configs = JsonConvert.DeserializeObject<ServerParseConfig[]>(content)
|
||||
?? Array.Empty<ServerParseConfig>();
|
||||
|
||||
foreach (var config in configs)
|
||||
{
|
||||
var options = config.Configuration.Select(x => new ParseConfigOptionForm()
|
||||
{
|
||||
Key = x.Key,
|
||||
Value = x.Value
|
||||
}).ToList();
|
||||
|
||||
|
||||
Configs.Add(Mapper.Map<ParseConfigForm>(config), options);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async Task<string> ValidateAndGet()
|
||||
{
|
||||
await ValidatorHelper.ValidateRange(Configs.Keys);
|
||||
|
||||
foreach (var options in Configs.Values)
|
||||
await ValidatorHelper.ValidateRange(options);
|
||||
|
||||
var finalConfigs = Configs.Select(x => new ServerParseConfig()
|
||||
{
|
||||
File = x.Key.File,
|
||||
Type = x.Key.Type,
|
||||
Configuration = x.Value.ToDictionary(y => y.Key, y => y.Value)
|
||||
}).ToList();
|
||||
|
||||
return JsonConvert.SerializeObject(finalConfigs);
|
||||
}
|
||||
|
||||
private async Task AddConfig()
|
||||
{
|
||||
Configs.Add(new(), new());
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task RemoveConfig(ParseConfigForm config)
|
||||
{
|
||||
Configs.Remove(config);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task AddOption(ParseConfigForm config)
|
||||
{
|
||||
Configs[config].Add(new());
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task RemoveOption(ParseConfigForm config, ParseConfigOptionForm option)
|
||||
{
|
||||
Configs[config].Remove(option);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
129
Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor
Normal file
129
Moonlight/Features/Servers/UI/Views/Admin/Images/Index.razor
Normal file
|
@ -0,0 +1,129 @@
|
|||
@page "/admin/servers/images"
|
||||
|
||||
@using BlazorTable
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.Core.Exceptions
|
||||
@using Moonlight.Core.Repositories
|
||||
@using Moonlight.Core.Services.Interop
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
|
||||
@inject Repository<ServerImage> ImageRepository
|
||||
@inject Repository<ServerImageVariable> ImageVariableRepository
|
||||
@inject Repository<ServerDockerImage> DockerImageRepository
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject ToastService ToastService
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Manage server images</h3>
|
||||
<div class="card-toolbar">
|
||||
<a href="/admin/servers/images/new" class="btn btn-icon btn-success ms-3">
|
||||
<i class="bx bx-sm bx-plus"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<Table TableItem="ServerImage"
|
||||
Items="Images"
|
||||
PageSize="50"
|
||||
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||
TableHeadClass="fw-bold text-muted">
|
||||
<Column TableItem="ServerImage" Field="@(x => x.Id)" Title="Id"/>
|
||||
<Column TableItem="ServerImage" Field="@(x => x.Name)" Title="Name"/>
|
||||
<Column TableItem="ServerImage" Field="@(x => x.Author)" Title="Author"/>
|
||||
<Column TableItem="ServerImage" Field="@(x => x.Id)" Title="">
|
||||
<Template>
|
||||
@if (!string.IsNullOrEmpty(context.UpdateUrl))
|
||||
{
|
||||
<WButton OnClick="() => Update(context)" CssClasses="btn btn-sm btn-primary">
|
||||
<span>Update</span>
|
||||
<i class="bx bx-sm bx-refresh"></i>
|
||||
</WButton>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(context.DonateUrl))
|
||||
{
|
||||
<a href="@(context.DonateUrl)" target="_blank" class="btn btn-sm btn-info">
|
||||
<span>Donate</span>
|
||||
<i class="bx bx-sm bxs-heart text-danger"></i>
|
||||
</a>
|
||||
}
|
||||
</Template>
|
||||
</Column>
|
||||
<Column TableItem="ServerImage" Field="@(x => x.Id)" Title="" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="text-end">
|
||||
<div class="btn-group">
|
||||
<a href="/admin/servers/images/view/@(context.Id)" class="btn btn-icon btn-warning">
|
||||
<i class="bx bx-sm bx-slider"></i>
|
||||
</a>
|
||||
<ConfirmButton OnClick="() => Delete(context)" CssClasses="btn btn-icon btn-danger">
|
||||
<i class="bx bx-sm bx-trash"></i>
|
||||
</ConfirmButton>
|
||||
</div>
|
||||
</div>
|
||||
</Template>
|
||||
</Column>
|
||||
</Table>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private LazyLoader LazyLoader;
|
||||
private ServerImage[] Images;
|
||||
|
||||
private Task Load(LazyLoader arg)
|
||||
{
|
||||
Images = ImageRepository
|
||||
.Get()
|
||||
.ToArray();
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task Update(ServerImage image)
|
||||
{
|
||||
}
|
||||
|
||||
private async Task Delete(ServerImage image)
|
||||
{
|
||||
var anyServerWithThisImage = ServerRepository
|
||||
.Get()
|
||||
.Any(x => x.Image.Id == image.Id);
|
||||
|
||||
if (anyServerWithThisImage)
|
||||
throw new DisplayException("This image cannot be deleted, because one or more server depend on it. Delete the server(s) first and then delete the image");
|
||||
|
||||
var imageWithData = ImageRepository
|
||||
.Get()
|
||||
.Include(x => x.Variables)
|
||||
.Include(x => x.DockerImages)
|
||||
.First(x => x.Id == image.Id);
|
||||
|
||||
// Cache and clear relational data
|
||||
|
||||
var variables = imageWithData.Variables.ToArray();
|
||||
var dockerImages = imageWithData.DockerImages.ToArray();
|
||||
|
||||
imageWithData.DockerImages.Clear();
|
||||
imageWithData.Variables.Clear();
|
||||
|
||||
ImageRepository.Update(imageWithData);
|
||||
|
||||
foreach (var variable in variables)
|
||||
ImageVariableRepository.Delete(variable);
|
||||
|
||||
foreach (var dockerImage in dockerImages)
|
||||
DockerImageRepository.Delete(dockerImage);
|
||||
|
||||
// And now we can clear the image
|
||||
|
||||
ImageRepository.Delete(imageWithData);
|
||||
|
||||
// and notify the user
|
||||
await ToastService.Success("Successfully deleted image");
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
10
Moonlight/Features/Servers/UI/Views/Admin/Images/New.razor
Normal file
10
Moonlight/Features/Servers/UI/Views/Admin/Images/New.razor
Normal file
|
@ -0,0 +1,10 @@
|
|||
@page "/admin/servers/images/new"
|
||||
|
||||
@using Moonlight.Features.Servers.Entities
|
||||
|
||||
|
||||
|
||||
@code
|
||||
{
|
||||
private ServerImage Image;
|
||||
}
|
Loading…
Reference in a new issue