Add UI for editing Docker hosts, add database migration

This commit is contained in:
Pat Hartl 2025-03-23 20:19:18 -05:00
parent 98cab7ec24
commit b5dbda3eec
9 changed files with 3086 additions and 120 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddDockerHostProperties : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ContainerId",
table: "Servers",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "DockerHostId",
table: "Servers",
type: "TEXT",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<int>(
name: "Engine",
table: "Servers",
type: "INTEGER",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ContainerId",
table: "Servers");
migrationBuilder.DropColumn(
name: "DockerHostId",
table: "Servers");
migrationBuilder.DropColumn(
name: "Engine",
table: "Servers");
}
}
}

View file

@ -1041,6 +1041,28 @@ namespace LANCommander.Migrations
b.ToTable("Roles", (string)null);
});
modelBuilder.Entity("LANCommander.Server.Data.Models.RoleClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("LANCommander.Server.Data.Models.SavePath", b =>
{
b.Property<Guid>("Id")
@ -1164,12 +1186,23 @@ namespace LANCommander.Migrations
b.Property<int>("AutostartMethod")
.HasColumnType("INTEGER");
b.Property<string>("ContainerId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("DockerHostId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Engine")
.HasColumnType("INTEGER");
b.Property<Guid?>("GameId")
.HasColumnType("TEXT");
@ -1487,6 +1520,28 @@ namespace LANCommander.Migrations
b.ToTable("Users", (string)null);
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserCustomField", b =>
{
b.Property<Guid>("Id")
@ -1529,66 +1584,7 @@ namespace LANCommander.Migrations
b.ToTable("UserCustomField");
});
modelBuilder.Entity("LibraryGame", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("LibraryId")
.HasColumnType("TEXT");
b.HasKey("GameId", "LibraryId");
b.HasIndex("LibraryId");
b.ToTable("LibraryGame");
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("RoleId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ClaimType")
.HasColumnType("TEXT");
b.Property<string>("ClaimValue")
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
modelBuilder.Entity("LANCommander.Server.Data.Models.UserLogin", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("TEXT");
@ -1609,7 +1605,7 @@ namespace LANCommander.Migrations
b.ToTable("UserLogins", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
modelBuilder.Entity("LANCommander.Server.Data.Models.UserRole", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
@ -1624,7 +1620,7 @@ namespace LANCommander.Migrations
b.ToTable("UserRoles", (string)null);
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
modelBuilder.Entity("LANCommander.Server.Data.Models.UserToken", b =>
{
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
@ -1643,6 +1639,21 @@ namespace LANCommander.Migrations
b.ToTable("UserTokens", (string)null);
});
modelBuilder.Entity("LibraryGame", b =>
{
b.Property<Guid>("GameId")
.HasColumnType("TEXT");
b.Property<Guid>("LibraryId")
.HasColumnType("TEXT");
b.HasKey("GameId", "LibraryId");
b.HasIndex("LibraryId");
b.ToTable("LibraryGame");
});
modelBuilder.Entity("PageGame", b =>
{
b.Property<Guid>("GameId")
@ -2345,6 +2356,15 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Server.Data.Models.RoleClaim", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LANCommander.Server.Data.Models.SavePath", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", "CreatedBy")
@ -2530,6 +2550,15 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserClaim", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserCustomField", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", "CreatedBy")
@ -2555,6 +2584,43 @@ namespace LANCommander.Migrations
b.Navigation("User");
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserLogin", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserRole", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Role", "Role")
.WithMany("UserRoles")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Server.Data.Models.User", "User")
.WithMany("UserRoles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("LANCommander.Server.Data.Models.UserToken", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("LibraryGame", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Game", null)
@ -2570,57 +2636,6 @@ namespace LANCommander.Migrations
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Role", null)
.WithMany()
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
{
b.HasOne("LANCommander.Server.Data.Models.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("PageGame", b =>
{
b.HasOne("LANCommander.Server.Data.Models.Game", null)
@ -2737,6 +2752,11 @@ namespace LANCommander.Migrations
b.Navigation("Scripts");
});
modelBuilder.Entity("LANCommander.Server.Data.Models.Role", b =>
{
b.Navigation("UserRoles");
});
modelBuilder.Entity("LANCommander.Server.Data.Models.Server", b =>
{
b.Navigation("Actions");
@ -2769,6 +2789,8 @@ namespace LANCommander.Migrations
b.Navigation("Media");
b.Navigation("PlaySessions");
b.Navigation("UserRoles");
});
#pragma warning restore 612, 618
}

View file

@ -0,0 +1,7 @@
namespace LANCommander.Server.Data.Enums;
public enum ServerEngine
{
Local,
Docker
}

View file

@ -1,12 +1,14 @@
using LANCommander.SDK.Enums;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using LANCommander.Server.Data.Enums;
namespace LANCommander.Server.Data.Models
{
public class Server : BaseModel
{
public string Name { get; set; } = "";
public ServerEngine Engine { get; set; } = ServerEngine.Local;
public string Path { get; set; } = "";
public string Arguments { get; set; } = "";
public string WorkingDirectory { get; set; } = "";
@ -14,6 +16,7 @@ namespace LANCommander.Server.Data.Models
public string OnStartScriptPath { get; set; } = "";
public string OnStopScriptPath { get; set; } = "";
public string DockerHostId { get; set; }
public string ContainerId { get; set; } = "";
public string Host { get; set; } = "";
public int Port { get; set; } = 0;

View file

@ -4,6 +4,7 @@ using LANCommander.Server.Services.Factories;
using LANCommander.Server.Services.Importers;
using LANCommander.Server.Services.MediaGrabbers;
using LANCommander.Server.Services.Models;
using LANCommander.Server.Services.ServerEngines;
using Microsoft.Extensions.DependencyInjection;
namespace LANCommander.Server.Services.Extensions;
@ -51,6 +52,14 @@ public static class IServiceCollectionExtensions
services.AddScoped<IImporter<Data.Models.Game>, GameImporter>();
services.AddScoped<IImporter<Data.Models.Server>, ServerImporter>();
services.AddScoped<IImporter<Data.Models.Redistributable>, RedistributableImporter>();
// Register server engines
services.AddKeyedSingleton<IServerEngine, LocalServerEngine>("Local");
foreach (var dockerHost in settings.Servers.DockerHosts)
{
services.AddKeyedSingleton<IServerEngine, DockerServerEngine>(dockerHost.Id.ToString());
}
services.AddSingleton<ServerProcessService>();
services.AddSingleton<IPXRelayService>();

View file

@ -186,11 +186,12 @@ namespace LANCommander.Server.Services.Models
public class ServerSettings
{
public string StoragePath { get; set; } = "Servers";
public IEnumerable<DockerHostConfiguration> Hosts { get; set; } = new List<DockerHostConfiguration>();
public IEnumerable<DockerHostConfiguration> DockerHosts { get; set; } = new List<DockerHostConfiguration>();
}
public class DockerHostConfiguration
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Name { get; set; } = "Local";
public string Address { get; set; } = "/var/run/docker.sock";
}

View file

@ -0,0 +1,62 @@
<Flex Direction="FlexDirection.Vertical" Gap="FlexGap.Large">
@if (!Settings.Servers.DockerHosts.Any())
{
<Empty Description="@("No Docker hosts have been configured")" />
}
<Collapse>
@foreach (var dockerHost in Settings.Servers.DockerHosts)
{
<Panel Header="@dockerHost.Name">
<ExtraTemplate>
<Button Type="ButtonType.Text" Icon="@IconType.Outline.Close" Size="ButtonSize.Small" Danger OnClick="() => Remove(dockerHost)"/>
</ExtraTemplate>
<ChildContent>
<Form Model="dockerHost" Layout="FormLayout.Vertical">
<FormItem Label="Name">
<Input @bind-Value="context.Name" BindOnInput />
</FormItem>
<FormItem Label="Address">
<Input @bind-Value="context.Address" />
</FormItem>
</Form>
</ChildContent>
</Panel>
}
</Collapse>
<Flex Justify="FlexJustify.Center">
<Button OnClick="Add" Type="ButtonType.Primary">Add Host</Button>
</Flex>
</Flex>
@code {
[Parameter] public IEnumerable<DockerHostConfiguration> Values { get; set; }
[Parameter] public EventCallback<IEnumerable<DockerHostConfiguration>> ValuesChanged { get; set; }
Settings Settings = SettingService.GetSettings();
List<DockerHostConfiguration> DockerHosts = new();
protected override void OnParametersSet()
{
DockerHosts = Values.ToList();
}
async Task Add()
{
DockerHosts.Add(new DockerHostConfiguration());
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(DockerHosts);
}
async Task Remove(DockerHostConfiguration dockerHost)
{
DockerHosts.Remove(dockerHost);
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(DockerHosts);
}
}

View file

@ -1,20 +1,25 @@
@page "/Settings/Servers"
@using LANCommander.Server.UI.Pages.Settings.Components
@inject IMessageService MessageService
@inject ILogger<Servers> Logger
@attribute [Authorize(Roles = RoleService.AdministratorRoleName)]
<PageHeader Title="Servers" />
<PageHeader Title="Servers">
<PageHeaderExtra>
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
</PageHeaderExtra>
</PageHeader>
<PageContent>
<Form Model="Settings" Layout="@FormLayout.Vertical">
<FormItem Label="Storage Path">
<FilePicker Root="@RootPath" EntrySelectable="x => x is FileManagerDirectory" @bind-Value="@context.Servers.StoragePath" OkText="Select Path" Title="Choose Path" OnSelected="OnPathSelected" />
</FormItem>
<FormItem>
<Button OnClick="Save" Type="@ButtonType.Primary">Save</Button>
</FormItem>
</Form>
<Divider Text="Docker Hosts" />
<ServerEngineEditor @bind-Values="Settings.Servers.DockerHosts" />
</PageContent>
@code {
@ -26,7 +31,12 @@
{
try
{
SettingService.SaveSettings(Settings);
var settings = SettingService.GetSettings();
settings.Servers = Settings.Servers;
SettingService.SaveSettings(settings);
MessageService.Success("Settings saved!");
}
catch (Exception ex)