Started implementing image editor and parse config editor

This commit is contained in:
Marcel Baumgartner 2024-01-28 13:58:23 +01:00
parent 6fd1336f1c
commit 99a7d7bd73
13 changed files with 1859 additions and 1 deletions

File diff suppressed because it is too large Load diff

View file

@ -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");
}
}
}

View file

@ -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 =>

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

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

View file

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

View file

@ -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; } = "";
}

View file

@ -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; } = "";
}

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

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

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

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

View file

@ -0,0 +1,10 @@
@page "/admin/servers/images/new"
@using Moonlight.Features.Servers.Entities
@code
{
private ServerImage Image;
}