Added custom layout options for the server list
This commit is contained in:
@@ -23,6 +23,8 @@ public class User
|
||||
public string State { get; set; } = "";
|
||||
|
||||
public string Country { get; set; } = "";
|
||||
|
||||
public string ServerListLayoutJson { get; set; } = "";
|
||||
|
||||
// States
|
||||
|
||||
|
||||
1077
Moonlight/App/Database/Migrations/20230623235512_AddedServerListLayoutToUser.Designer.cs
generated
Normal file
1077
Moonlight/App/Database/Migrations/20230623235512_AddedServerListLayoutToUser.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Moonlight.App.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedServerListLayoutToUser : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "ServerListLayoutJson",
|
||||
table: "Users",
|
||||
type: "longtext",
|
||||
nullable: false)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ServerListLayoutJson",
|
||||
table: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -780,6 +780,10 @@ namespace Moonlight.App.Database.Migrations
|
||||
b.Property<int>("Rating")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("ServerListLayoutJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
b.Property<string>("State")
|
||||
.IsRequired()
|
||||
.HasColumnType("longtext");
|
||||
|
||||
7
Moonlight/App/Models/Misc/ServerGroup.cs
Normal file
7
Moonlight/App/Models/Misc/ServerGroup.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Moonlight.App.Models.Misc;
|
||||
|
||||
public class ServerGroup
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public List<string> Servers { get; set; } = new();
|
||||
}
|
||||
@@ -103,6 +103,8 @@
|
||||
<script src="/_content/CurrieTechnologies.Razor.SweetAlert2/sweetAlert2.min.js"></script>
|
||||
<script src="/_content/Blazor.ContextMenu/blazorContextMenu.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/@@shopify/draggable@1.0.0-beta.11/lib/draggable.bundle.js"></script>
|
||||
|
||||
<script src="https://www.google.com/recaptcha/api.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.min.js"></script>
|
||||
|
||||
@@ -1,13 +1,414 @@
|
||||
@page "/servers"
|
||||
@using Moonlight.App.Repositories.Servers
|
||||
@using Microsoft.EntityFrameworkCore
|
||||
@using Moonlight.App.Database.Entities
|
||||
@using Moonlight.App.Helpers
|
||||
@using Moonlight.App.Models.Misc
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services
|
||||
@using Newtonsoft.Json
|
||||
|
||||
@inject ServerRepository ServerRepository
|
||||
@inject Repository<Server> ServerRepository
|
||||
@inject Repository<User> UserRepository
|
||||
@inject SmartTranslateService SmartTranslateService
|
||||
@inject IServiceScopeFactory ServiceScopeFactory
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<LazyLoader Load="Load">
|
||||
<LazyLoader @ref="LazyLoader" Load="Load">
|
||||
<div class="mx-auto">
|
||||
<div class="card card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="badge badge-primary badge-lg px-5 me-4">Beta</span>
|
||||
@if (EditMode)
|
||||
{
|
||||
<div>
|
||||
<WButton Text="@(SmartTranslateService.Translate("Finish editing layout"))"
|
||||
CssClasses="btn-secondary"
|
||||
OnClick="async () => await SetEditMode(false)">
|
||||
</WButton>
|
||||
<WButton Text="@(SmartTranslateService.Translate("New group"))"
|
||||
WorkingText=""
|
||||
CssClasses="btn-primary"
|
||||
OnClick="AddGroup">
|
||||
</WButton>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<WButton Text="@(SmartTranslateService.Translate("Edit layout"))"
|
||||
CssClasses="btn-secondary"
|
||||
OnClick="async () => await SetEditMode(true)">
|
||||
</WButton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@foreach (var group in ServerGroups)
|
||||
{
|
||||
@*
|
||||
<div class="card my-2">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
@if (EditMode)
|
||||
{
|
||||
<input @bind="group.Name" class="form-control"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(group.Name)</span>
|
||||
}
|
||||
</div>
|
||||
@if (EditMode)
|
||||
{
|
||||
<div class="card-toolbar">
|
||||
<WButton Text="@(SmartTranslateService.Translate("Remove group"))"
|
||||
WorkingText=""
|
||||
CssClasses="btn-danger"
|
||||
OnClick="async () => await RemoveGroup(group)">
|
||||
</WButton>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row min-h-200px draggable-zone" ml-server-group="@(group.Name)">
|
||||
@foreach (var id in group.Servers)
|
||||
{
|
||||
var server = AllServers.First(x => x.Id.ToString() == id);
|
||||
|
||||
<div class="col-12 col-md-3 p-3 draggable" ml-server-id="@(server.Id)">
|
||||
<div class="card bg-secondary">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<h3 class="card-label">@(server.Name)</h3>
|
||||
</div>
|
||||
@if (EditMode)
|
||||
{
|
||||
<div class="card-toolbar">
|
||||
<a href="#" class="btn btn-icon btn-sm btn-hover-light-primary draggable-handle">
|
||||
<i class="bx bx-md bx-move"></i>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (EditMode)
|
||||
{
|
||||
<TL>Hidden in edit mode</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>*@
|
||||
<div class="accordion my-3" id="serverListGroup@(group.GetHashCode())">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="serverListGroup-header@(group.GetHashCode())">
|
||||
<button class="accordion-button fs-4 fw-semibold collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#serverListGroup-body@(group.GetHashCode())" aria-expanded="false" aria-controls="serverListGroup-body@(group.GetHashCode())">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
@if (EditMode)
|
||||
{
|
||||
<input @bind="group.Name" class="form-control"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (string.IsNullOrEmpty(group.Name))
|
||||
{
|
||||
<TL>Unsorted servers</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@(group.Name)</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
@if (EditMode)
|
||||
{
|
||||
<WButton Text="@(SmartTranslateService.Translate("Remove group"))"
|
||||
WorkingText=""
|
||||
CssClasses="btn-danger"
|
||||
OnClick="async () => await RemoveGroup(group)">
|
||||
</WButton>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="serverListGroup-body@(group.GetHashCode())" class="accordion-collapse collapse" aria-labelledby="serverListGroup-header@(group.GetHashCode())" data-bs-parent="#serverListGroup">
|
||||
<div class="accordion-body">
|
||||
<div class="row min-h-200px draggable-zone" ml-server-group="@(group.Name)">
|
||||
@foreach (var id in group.Servers)
|
||||
{
|
||||
var server = AllServers.First(x => x.Id.ToString() == id);
|
||||
|
||||
<div class="col-12 col-md-3 p-3 draggable" ml-server-id="@(server.Id)">
|
||||
<a class="invisible-a" href="/server/@(server.Uuid)">
|
||||
<div class="card bg-secondary">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<span class="card-label">@(server.Name)</span>
|
||||
</div>
|
||||
@if (EditMode)
|
||||
{
|
||||
<div class="card-toolbar">
|
||||
<a href="#" class="btn btn-icon btn-sm btn-hover-light-primary draggable-handle">
|
||||
<i class="bx bx-md bx-move"></i>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (EditMode)
|
||||
{
|
||||
<TL>Hidden in edit mode</TL>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="card-text fs-6">
|
||||
@(Math.Round(server.Memory / 1024D, 2)) GB / @(Math.Round(server.Disk / 1024D, 2)) GB / @(server.Node.Name) <span class="text-gray-700">- @(server.Image.Name)</span>
|
||||
</span>
|
||||
<div class="card-text my-1 fs-6 fw-bold">
|
||||
@(server.Node.Fqdn):@(server.MainAllocation.Port)
|
||||
</div>
|
||||
<div class="card-text fs-6">
|
||||
@if (StatusCache.ContainsKey(server))
|
||||
{
|
||||
var status = StatusCache[server];
|
||||
|
||||
switch (status)
|
||||
{
|
||||
case "offline":
|
||||
<span class="text-danger">
|
||||
<TL>Offline</TL>
|
||||
</span>
|
||||
break;
|
||||
case "stopping":
|
||||
<span class="text-warning">
|
||||
<TL>Stopping</TL>
|
||||
</span>
|
||||
break;
|
||||
case "starting":
|
||||
<span class="text-warning">
|
||||
<TL>Starting</TL>
|
||||
</span>
|
||||
break;
|
||||
case "running":
|
||||
<span class="text-success">
|
||||
<TL>Running</TL>
|
||||
</span>
|
||||
break;
|
||||
case "failed":
|
||||
<span class="text-gray-400">
|
||||
<TL>Failed</TL>
|
||||
</span>
|
||||
break;
|
||||
default:
|
||||
<span class="text-danger">
|
||||
<TL>Offline</TL>
|
||||
</span>
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-gray-400">
|
||||
<TL>Loading</TL>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private Server[] AllServers;
|
||||
private LazyLoader LazyLoader;
|
||||
private readonly Dictionary<Server, string> StatusCache = new();
|
||||
|
||||
private List<ServerGroup> ServerGroups = new();
|
||||
private bool EditMode = false;
|
||||
|
||||
private async Task Load(LazyLoader arg)
|
||||
{
|
||||
AllServers = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.MainAllocation)
|
||||
.Include(x => x.Node)
|
||||
.Include(x => x.Image)
|
||||
.Where(x => x.Owner.Id == User.Id)
|
||||
.OrderBy(x => x.Name)
|
||||
.ToArray();
|
||||
|
||||
|
||||
if (string.IsNullOrEmpty(User.ServerListLayoutJson))
|
||||
{
|
||||
ServerGroups.Add(new()
|
||||
{
|
||||
Name = "",
|
||||
Servers = AllServers.Select(x => x.Id.ToString()).ToList()
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerGroups = (JsonConvert.DeserializeObject<ServerGroup[]>(
|
||||
User.ServerListLayoutJson) ?? Array.Empty<ServerGroup>()).ToList();
|
||||
}
|
||||
|
||||
foreach (var server in AllServers)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
|
||||
|
||||
AddStatus(server, (await serverService.GetDetails(server)).State);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
AddStatus(server, "failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddGroup()
|
||||
{
|
||||
ServerGroups.Insert(0, new()
|
||||
{
|
||||
Name = "New group"
|
||||
});
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
|
||||
}
|
||||
|
||||
private async Task RemoveGroup(ServerGroup group)
|
||||
{
|
||||
ServerGroups.Remove(group);
|
||||
await EnsureAllServersInGroups();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
|
||||
}
|
||||
|
||||
private async Task SetEditMode(bool toggle)
|
||||
{
|
||||
EditMode = toggle;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (EditMode)
|
||||
{
|
||||
await EnsureAllServersInGroups();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("moonlight.serverList.init");
|
||||
}
|
||||
else
|
||||
{
|
||||
var json = JsonConvert.SerializeObject(await GetGroupsFromClient());
|
||||
User.ServerListLayoutJson = json;
|
||||
UserRepository.Update(User);
|
||||
|
||||
await LazyLoader.Reload();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ServerGroup[]> GetGroupsFromClient()
|
||||
{
|
||||
var serverGroups = await JsRuntime.InvokeAsync<ServerGroup[]>("moonlight.serverList.getData");
|
||||
|
||||
// Check user data to prevent users from doing stupid stuff
|
||||
foreach (var serverGroup in serverGroups)
|
||||
{
|
||||
if (serverGroup.Name.Length > 30)
|
||||
{
|
||||
Logger.Verbose("Server list group lenght too long");
|
||||
return Array.Empty<ServerGroup>();
|
||||
}
|
||||
|
||||
if (serverGroup.Servers.Any(x => AllServers.All(y => y.Id.ToString() != x)))
|
||||
{
|
||||
Logger.Verbose("User tried to add a server in his server list which he has no access to");
|
||||
return Array.Empty<ServerGroup>();
|
||||
}
|
||||
}
|
||||
|
||||
return serverGroups;
|
||||
}
|
||||
|
||||
private Task EnsureAllServersInGroups()
|
||||
{
|
||||
var presentInGroup = new List<Server>();
|
||||
|
||||
foreach (var group in ServerGroups)
|
||||
{
|
||||
foreach (var id in group.Servers)
|
||||
presentInGroup.Add(AllServers.First(x => x.Id.ToString() == id));
|
||||
}
|
||||
|
||||
var serversMissing = new List<Server>();
|
||||
|
||||
foreach (var server in AllServers)
|
||||
{
|
||||
if (presentInGroup.All(x => x.Id != server.Id))
|
||||
serversMissing.Add(server);
|
||||
}
|
||||
|
||||
if (serversMissing.Any())
|
||||
{
|
||||
var defaultGroup = ServerGroups.FirstOrDefault(x => x.Name == "");
|
||||
|
||||
if (defaultGroup == null)
|
||||
{
|
||||
defaultGroup = new ServerGroup()
|
||||
{
|
||||
Name = ""
|
||||
};
|
||||
|
||||
ServerGroups.Add(defaultGroup);
|
||||
}
|
||||
|
||||
foreach (var server in serversMissing)
|
||||
defaultGroup.Servers.Add(server.Id.ToString());
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void AddStatus(Server server, string status)
|
||||
{
|
||||
lock (StatusCache)
|
||||
{
|
||||
StatusCache.Add(server, status);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@*
|
||||
@if (AllServers.Any())
|
||||
{
|
||||
if (UseSortedServerView)
|
||||
@@ -207,57 +608,4 @@ else
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
public User User { get; set; }
|
||||
|
||||
private Server[] AllServers;
|
||||
private readonly Dictionary<Server, string> StatusCache = new();
|
||||
|
||||
private bool UseSortedServerView = false;
|
||||
|
||||
private Task Load(LazyLoader arg)
|
||||
{
|
||||
AllServers = ServerRepository
|
||||
.Get()
|
||||
.Include(x => x.Owner)
|
||||
.Include(x => x.MainAllocation)
|
||||
.Include(x => x.Node)
|
||||
.Include(x => x.Image)
|
||||
.Where(x => x.Owner.Id == User.Id)
|
||||
.OrderBy(x => x.Name)
|
||||
.ToArray();
|
||||
|
||||
foreach (var server in AllServers)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
|
||||
|
||||
AddStatus(server, (await serverService.GetDetails(server)).State);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
AddStatus(server, "failed");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void AddStatus(Server server, string status)
|
||||
{
|
||||
lock (StatusCache)
|
||||
{
|
||||
StatusCache.Add(server, status);
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
*@
|
||||
@@ -354,8 +354,6 @@
|
||||
moonlight.keyListener.listener = (event) =>
|
||||
{
|
||||
// filter here what key events should be sent to moonlight
|
||||
|
||||
console.log(event);
|
||||
|
||||
if(event.code === "KeyS" && event.ctrlKey)
|
||||
{
|
||||
@@ -370,5 +368,54 @@
|
||||
{
|
||||
window.removeEventListener('keydown', moonlight.keyListener.listener);
|
||||
}
|
||||
},
|
||||
serverList: {
|
||||
init: function ()
|
||||
{
|
||||
if(moonlight.serverList.Swappable)
|
||||
{
|
||||
moonlight.serverList.Swappable.destroy();
|
||||
}
|
||||
|
||||
let containers = document.querySelectorAll(".draggable-zone");
|
||||
|
||||
if (containers.length !== 0)
|
||||
{
|
||||
moonlight.serverList.Swappable = new Draggable.Sortable(containers, {
|
||||
draggable: ".draggable",
|
||||
handle: ".draggable .draggable-handle",
|
||||
mirror: {
|
||||
//appendTo: selector,
|
||||
appendTo: "body",
|
||||
constrainDimensions: true
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
getData: function ()
|
||||
{
|
||||
let groups = new Array();
|
||||
|
||||
let groupElements = document.querySelectorAll('[ml-server-group]');
|
||||
|
||||
groupElements.forEach(groupElement => {
|
||||
let group = new Object();
|
||||
group.name = groupElement.attributes.getNamedItem("ml-server-group").value;
|
||||
|
||||
let servers = new Array();
|
||||
let serverElements = groupElement.querySelectorAll("[ml-server-id]");
|
||||
|
||||
serverElements.forEach(serverElement => {
|
||||
let id = serverElement.attributes.getNamedItem("ml-server-id").value;
|
||||
|
||||
servers.push(id);
|
||||
});
|
||||
|
||||
group.servers = servers;
|
||||
groups.push(group);
|
||||
});
|
||||
|
||||
return groups;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user