Files
Moonlight/Moonlight/Shared/Views/Servers/Index.razor
2023-06-24 02:35:01 +02:00

611 lines
27 KiB
Plaintext

@page "/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 Repository<Server> ServerRepository
@inject Repository<User> UserRepository
@inject SmartTranslateService SmartTranslateService
@inject IServiceScopeFactory ServiceScopeFactory
@inject IJSRuntime JsRuntime
<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)
{
var groupedServers = AllServers
.OrderBy(x => x.Name)
.GroupBy(x => x.Image.Name);
foreach (var groupedServer in groupedServers)
{
<div class="separator separator-content my-15">@(groupedServer.Key)</div>
<div class="card card-body bg-secondary py-0 my-0 mx-0 px-0">
@foreach (var server in groupedServer)
{
<div class="row mx-4 my-4">
<a class="card card-body" href="/server/@(server.Uuid)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-server"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/server/@(server.Uuid)" class="text-gray-800 text-hover-primary mb-1 fs-5">
@(server.Name)
</a>
<span class="text-gray-400 fw-semibold d-block 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>
</div>
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="d-none d-sm-block col my-auto 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>
</a>
</div>
}
</div>
}
}
else
{
foreach (var server in AllServers)
{
<div class="row px-5 mb-5">
<a class="card card-body" href="/server/@(server.Uuid)">
<div class="row">
<div class="col">
<div class="d-flex align-items-center">
<div class="symbol symbol-50px me-3">
<i class="bx bx-md bx-server"></i>
</div>
<div class="d-flex justify-content-start flex-column">
<a href="/server/@(server.Uuid)" class="text-gray-800 text-hover-primary mb-1 fs-5">
@(server.Name)
</a>
<span class="text-gray-400 fw-semibold d-block 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>
</div>
</div>
<div class="d-none d-sm-block col my-auto fs-6">
@(server.Node.Fqdn):@(server.MainAllocation.Port)
</div>
<div class="d-none d-sm-block col my-auto 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>
</a>
</div>
}
}
}
else
{
<div class="alert bg-info d-flex flex-column flex-sm-row w-100 p-5">
<div class="d-flex flex-column pe-0 pe-sm-10">
<h4 class="fw-semibold">
<TL>You have no servers</TL>
</h4>
<span>
<TL>We were not able to find any servers associated with your account</TL>
</span>
</div>
</div>
}
<div class="row mt-7 px-3">
<div class="card">
<div class="card-header">
<div class="card-title">
<span class="badge badge-primary badge-lg">Beta</span>
</div>
</div>
<div class="card-body">
<div class="row">
<label class="col-lg-4 col-form-label fw-semibold fs-6">Sorted server view</label>
<div class="col-lg-8 d-flex align-items-center">
<div class="form-check form-check-solid form-switch form-check-custom fv-row">
<input class="form-check-input w-45px h-30px" type="checkbox" id="sortedServerView" @bind="UseSortedServerView">
<label class="form-check-label" for="sortedServerView"></label>
</div>
</div>
</div>
</div>
</div>
</div>
*@