Compare commits
16 Commits
2d1b48b0d4
...
v2.1
| Author | SHA1 | Date | |
|---|---|---|---|
| ecf0e01914 | |||
| 6d447a0ff9 | |||
| ba5e364c05 | |||
| 2a2ce28b5f | |||
| 91887ec047 | |||
| 2fc371c219 | |||
| 9470e06c0f | |||
| 2f8665f1d4 | |||
| 4d4f35e2be | |||
| 609ea3a443 | |||
| 3e19b29cde | |||
| 1475b89660 | |||
| 3bb9a08630 | |||
| 252c4103f3 | |||
| e7b1e77d0a | |||
| 7c5dc657dc |
69
.gitea/workflows/publish-nuget.yml
Normal file
69
.gitea/workflows/publish-nuget.yml
Normal file
@@ -0,0 +1,69 @@
|
||||
name: "Dev Publish: Nuget"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- v2.1
|
||||
paths:
|
||||
- 'MoonlightServers.*/*.csproj'
|
||||
|
||||
env:
|
||||
NUGET_SOURCE: https://git.battlestati.one/api/packages/Moonlight-Panel/nuget/index.json
|
||||
NUGET_PUBLIC: https://api.nuget.org/v3/index.json
|
||||
CONFIGURATION: Debug
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish ${{ matrix.project }}
|
||||
runs-on: linux_amd64
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
project:
|
||||
- MoonlightServers.Api
|
||||
- MoonlightServers.Shared
|
||||
- MoonlightServers.DaemonShared
|
||||
- MoonlightServers.Frontend
|
||||
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Restore NuGet packages
|
||||
run: >
|
||||
dotnet restore ${{ matrix.project }}
|
||||
--source ${{ env.NUGET_PUBLIC }}
|
||||
--source ${{ env.NUGET_SOURCE }}
|
||||
|
||||
# Frontend requires a host build + Tailwind compilation first
|
||||
- name: Build frontend host (Frontend only)
|
||||
if: matrix.project == 'MoonlightServers.Frontend'
|
||||
run: >
|
||||
dotnet build Hosts/MoonlightServers.Frontend.Host
|
||||
--configuration ${{ env.CONFIGURATION }}
|
||||
|
||||
- name: Build Tailwind styles (Frontend only)
|
||||
if: matrix.project == 'MoonlightServers.Frontend'
|
||||
working-directory: Hosts/MoonlightServers.Frontend.Host/Styles
|
||||
run: npm install && npm run build
|
||||
|
||||
- name: Build project
|
||||
run: >
|
||||
dotnet build ${{ matrix.project }}
|
||||
--configuration ${{ env.CONFIGURATION }}
|
||||
--no-restore
|
||||
|
||||
- name: Pack NuGet package
|
||||
run: >
|
||||
dotnet pack ${{ matrix.project }}
|
||||
--configuration ${{ env.CONFIGURATION }}
|
||||
--output ./artifacts
|
||||
--no-build
|
||||
|
||||
- name: Push NuGet package
|
||||
run: >
|
||||
dotnet nuget push ./artifacts/*.nupkg
|
||||
--skip-duplicate
|
||||
--source ${{ env.NUGET_SOURCE }}
|
||||
--api-key ${{ secrets.ACCESS_TOKEN }}
|
||||
@@ -8,15 +8,14 @@
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.5"/>
|
||||
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
||||
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -9,15 +9,6 @@
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7240;http://localhost:5031",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.5"/>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.5" PrivateAssets="all"/>
|
||||
<PackageReference Include="SimplePlugin" Version="1.0.2"/>
|
||||
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2"/>
|
||||
</ItemGroup>
|
||||
|
||||
@@ -15,8 +15,8 @@ export default function extractTailwindClasses(opts = {}) {
|
||||
},
|
||||
OnceExit() {
|
||||
const classArray = Array.from(classSet).sort();
|
||||
fs.mkdirSync('../../../Servers.Frontend/Styles', { recursive: true });
|
||||
fs.writeFileSync('../../../Servers.Frontend/Styles/Servers.Frontend.map', classArray.join('\n'));
|
||||
fs.mkdirSync('../../../MoonlightServers.Frontend/Styles', { recursive: true });
|
||||
fs.writeFileSync('../../../MoonlightServers.Frontend/Styles/MoonlightServers.Frontend.map', classArray.join('\n'));
|
||||
console.log(`Extracted classes ${classArray.length}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
@import "../bin/ShadcnBlazor/default-theme.css";
|
||||
@import "./theme.css";
|
||||
|
||||
@source "../bin/Moonlight.Frontend/*.map";
|
||||
@source "../bin/**/*.map";
|
||||
|
||||
@source "../../../Moonlight.Api/**/*.razor";
|
||||
@source "../../../Moonlight.Api/**/*.cs";
|
||||
@source "../../../Moonlight.Api/**/*.html";
|
||||
@source "../../../MoonlightServers.Api/**/*.razor";
|
||||
@source "../../../MoonlightServers.Api/**/*.cs";
|
||||
@source "../../../MoonlightServers.Api/**/*.html";
|
||||
|
||||
@source "../../../Moonlight.Frontend/**/*.razor";
|
||||
@source "../../../Moonlight.Frontend/**/*.cs";
|
||||
@source "../../../Moonlight.Frontend/**/*.html";
|
||||
@source "../../../MoonlightServers.Frontend/**/*.razor";
|
||||
@source "../../../MoonlightServers.Frontend/**/*.cs";
|
||||
@source "../../../MoonlightServers.Frontend/**/*.html";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
|
||||
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script>
|
||||
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script>
|
||||
|
||||
<script src="/_content/Moonlight.Frontend/chart.umd.js" defer></script>
|
||||
|
||||
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
157
MoonlightServers.Api/Admin/Nodes/CrudController.cs
Normal file
157
MoonlightServers.Api/Admin/Nodes/CrudController.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Nodes;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/servers/nodes")]
|
||||
public class CrudController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<Node> DatabaseRepository;
|
||||
private readonly HybridCache Cache;
|
||||
|
||||
public CrudController(
|
||||
DatabaseRepository<Node> databaseRepository,
|
||||
HybridCache cache
|
||||
)
|
||||
{
|
||||
DatabaseRepository = databaseRepository;
|
||||
Cache = cache;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Permissions.Nodes.View)]
|
||||
public async Task<ActionResult<PagedData<NodeDto>>> GetAsync(
|
||||
[FromQuery] int startIndex,
|
||||
[FromQuery] int length,
|
||||
[FromQuery] FilterOptions? filterOptions
|
||||
)
|
||||
{
|
||||
// Validation
|
||||
if (startIndex < 0)
|
||||
return Problem("Invalid start index specified", statusCode: 400);
|
||||
|
||||
if (length is < 1 or > 100)
|
||||
return Problem("Invalid length specified");
|
||||
|
||||
// Query building
|
||||
|
||||
var query = DatabaseRepository
|
||||
.Query();
|
||||
|
||||
// Filters
|
||||
if (filterOptions != null)
|
||||
{
|
||||
foreach (var filterOption in filterOptions.Filters)
|
||||
{
|
||||
query = filterOption.Key switch
|
||||
{
|
||||
nameof(Node.Name) =>
|
||||
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
|
||||
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
var data = await query
|
||||
.OrderBy(x => x.Id)
|
||||
.ProjectToDto()
|
||||
.Skip(startIndex)
|
||||
.Take(length)
|
||||
.ToArrayAsync();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
return new PagedData<NodeDto>(data, total);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Nodes.View)]
|
||||
public async Task<ActionResult<NodeDto>> GetAsync([FromRoute] int id)
|
||||
{
|
||||
var node = await DatabaseRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (node == null)
|
||||
return Problem("No node with this id found", statusCode: 404);
|
||||
|
||||
return NodeMapper.ToDto(node);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = Permissions.Nodes.Create)]
|
||||
public async Task<ActionResult<NodeDto>> CreateAsync([FromBody] CreateNodeDto request)
|
||||
{
|
||||
var node = NodeMapper.ToEntity(request);
|
||||
|
||||
node.TokenId = GenerateString(10);
|
||||
node.Token = GenerateString(64);
|
||||
|
||||
var finalRole = await DatabaseRepository.AddAsync(node);
|
||||
|
||||
return NodeMapper.ToDto(finalRole);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Nodes.Edit)]
|
||||
public async Task<ActionResult<NodeDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateNodeDto request)
|
||||
{
|
||||
var node = await DatabaseRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (node == null)
|
||||
return Problem("No node with this id found", statusCode: 404);
|
||||
|
||||
NodeMapper.Merge(node, request);
|
||||
|
||||
await DatabaseRepository.UpdateAsync(node);
|
||||
|
||||
return NodeMapper.ToDto(node);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Nodes.Delete)]
|
||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||
{
|
||||
var node = await DatabaseRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (node == null)
|
||||
return Problem("No node with this id found", statusCode: 404);
|
||||
|
||||
await DatabaseRepository.RemoveAsync(node);
|
||||
|
||||
// Remove cache for node token auth scheme
|
||||
await Cache.RemoveAsync(string.Format(NodeTokenSchemeHandler.CacheKeyFormat, node.TokenId));
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static string GenerateString(int length)
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
var stringBuilder = new StringBuilder();
|
||||
var random = new Random();
|
||||
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
stringBuilder.Append(chars[random.Next(chars.Length)]);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString();
|
||||
}
|
||||
}
|
||||
51
MoonlightServers.Api/Admin/Nodes/HealthController.cs
Normal file
51
MoonlightServers.Api/Admin/Nodes/HealthController.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Nodes;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = Permissions.Nodes.View)]
|
||||
[Route("api/admin/servers/nodes/{id:int}/health")]
|
||||
public class HealthController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<Node> DatabaseRepository;
|
||||
private readonly NodeService NodeService;
|
||||
private readonly ILogger<HealthController> Logger;
|
||||
|
||||
public HealthController(
|
||||
DatabaseRepository<Node> databaseRepository,
|
||||
NodeService nodeService,
|
||||
ILogger<HealthController> logger
|
||||
)
|
||||
{
|
||||
DatabaseRepository = databaseRepository;
|
||||
NodeService = nodeService;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<NodeHealthDto>> GetAsync([FromRoute] int id)
|
||||
{
|
||||
var node = await DatabaseRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (node == null)
|
||||
return Problem("No node with this id found", statusCode: 404);
|
||||
|
||||
var health = await NodeService.GetHealthAsync(node);
|
||||
|
||||
return new NodeHealthDto()
|
||||
{
|
||||
StatusCode = health.StatusCode,
|
||||
RemoteStatusCode = health.Dto?.RemoteStatusCode ?? 0,
|
||||
IsHealthy = health is { StatusCode: >= 200 and <= 299, Dto.RemoteStatusCode: >= 200 and <= 299 }
|
||||
};
|
||||
}
|
||||
}
|
||||
20
MoonlightServers.Api/Admin/Nodes/NodeMapper.cs
Normal file
20
MoonlightServers.Api/Admin/Nodes/NodeMapper.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Nodes;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class NodeMapper
|
||||
{
|
||||
public static partial NodeDto ToDto(Node node);
|
||||
public static partial IQueryable<NodeDto> ProjectToDto(this IQueryable<Node> nodes);
|
||||
public static partial Node ToEntity(CreateNodeDto dto);
|
||||
public static partial void Merge([MappingTarget] Node node, UpdateNodeDto dto);
|
||||
|
||||
public static partial NodeStatisticsDto ToDto(SystemStatisticsDto dto);
|
||||
}
|
||||
145
MoonlightServers.Api/Admin/Nodes/NodeService.cs
Normal file
145
MoonlightServers.Api/Admin/Nodes/NodeService.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.DaemonShared.Http;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Nodes;
|
||||
|
||||
public class NodeService
|
||||
{
|
||||
private readonly IHttpClientFactory ClientFactory;
|
||||
private readonly ILogger<NodeService> Logger;
|
||||
public NodeService(IHttpClientFactory clientFactory, ILogger<NodeService> logger)
|
||||
{
|
||||
ClientFactory = clientFactory;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task<NodeHealthStatus> GetHealthAsync(Node node)
|
||||
{
|
||||
var client = ClientFactory.CreateClient();
|
||||
|
||||
var request = CreateBaseRequest(node, HttpMethod.Get, "api/health");
|
||||
|
||||
try
|
||||
{
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return new NodeHealthStatus((int)response.StatusCode, null);
|
||||
|
||||
try
|
||||
{
|
||||
var health = await response
|
||||
.Content
|
||||
.ReadFromJsonAsync<HealthDto>(SerializationContext.Default.Options);
|
||||
|
||||
return new NodeHealthStatus((int)response.StatusCode, health);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogTrace(e, "An unhandled error occured while processing health response of node {id}", node.Id);
|
||||
|
||||
return new NodeHealthStatus((int)response.StatusCode, null);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogTrace(e, "An error occured while fetching health status of node {id}", node.Id);
|
||||
return new NodeHealthStatus(0, null);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<SystemStatisticsDto> GetStatisticsAsync(Node node)
|
||||
{
|
||||
var client = ClientFactory.CreateClient();
|
||||
|
||||
var request = CreateBaseRequest(node, HttpMethod.Get, "api/system/statistics");
|
||||
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
await EnsureSuccessAsync(response);
|
||||
|
||||
return (await response.Content.ReadFromJsonAsync<SystemStatisticsDto>(SerializationContext.Default.Options))!;
|
||||
}
|
||||
|
||||
private static HttpRequestMessage CreateBaseRequest(
|
||||
Node node,
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
HttpMethod method,
|
||||
string endpoint
|
||||
)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
|
||||
request.Headers.Add(HeaderNames.Authorization, node.Token);
|
||||
request.RequestUri = new Uri(new Uri(node.HttpEndpointUrl), endpoint);
|
||||
request.Method = method;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async Task EnsureSuccessAsync(HttpResponseMessage message)
|
||||
{
|
||||
if (message.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var problemDetails = await message.Content.ReadFromJsonAsync<ProblemDetails>(
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (problemDetails == null)
|
||||
{
|
||||
// If we cant handle problem details, we handle it natively
|
||||
message.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse into exception
|
||||
throw new NodeException(
|
||||
problemDetails.Type,
|
||||
problemDetails.Title,
|
||||
problemDetails.Status,
|
||||
problemDetails.Detail,
|
||||
problemDetails.Errors
|
||||
);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// If we cant handle problem details, we handle it natively
|
||||
message.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public record NodeHealthStatus(int StatusCode, HealthDto? Dto);
|
||||
|
||||
public class NodeException : Exception
|
||||
{
|
||||
public string Type { get; }
|
||||
public string Title { get; }
|
||||
public int Status { get; }
|
||||
public string? Detail { get; }
|
||||
public Dictionary<string, string[]>? Errors { get; }
|
||||
|
||||
public NodeException(
|
||||
string type,
|
||||
string title,
|
||||
int status,
|
||||
string? detail = null,
|
||||
Dictionary<string, string[]>? errors = null)
|
||||
: base(detail ?? title)
|
||||
{
|
||||
Type = type;
|
||||
Title = title;
|
||||
Status = status;
|
||||
Detail = detail;
|
||||
Errors = errors;
|
||||
}
|
||||
}
|
||||
40
MoonlightServers.Api/Admin/Nodes/StatisticsController.cs
Normal file
40
MoonlightServers.Api/Admin/Nodes/StatisticsController.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Nodes;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Nodes;
|
||||
|
||||
[ApiController]
|
||||
[Authorize(Policy = Permissions.Nodes.View)]
|
||||
[Route("api/admin/servers/nodes/{id:int}/statistics")]
|
||||
public class StatisticsController : Controller
|
||||
{
|
||||
private readonly NodeService NodeService;
|
||||
private readonly DatabaseRepository<Node> NodeRepository;
|
||||
|
||||
public StatisticsController(NodeService nodeService, DatabaseRepository<Node> nodeRepository)
|
||||
{
|
||||
NodeService = nodeService;
|
||||
NodeRepository = nodeRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<NodeStatisticsDto>> GetAsync([FromRoute] int id)
|
||||
{
|
||||
var node = await NodeRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (node == null)
|
||||
return Problem("No node with this id found", statusCode: 404);
|
||||
|
||||
var statistics = await NodeService.GetStatisticsAsync(node);
|
||||
var dto = NodeMapper.ToDto(statistics);
|
||||
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
172
MoonlightServers.Api/Admin/Templates/CrudController.cs
Normal file
172
MoonlightServers.Api/Admin/Templates/CrudController.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/servers/templates")]
|
||||
public class CrudController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<Template> DatabaseRepository;
|
||||
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||
|
||||
public CrudController(
|
||||
DatabaseRepository<Template> databaseRepository,
|
||||
DatabaseRepository<TemplateDockerImage> dockerImageRepository
|
||||
)
|
||||
{
|
||||
DatabaseRepository = databaseRepository;
|
||||
DockerImageRepository = dockerImageRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Permissions.Templates.View)]
|
||||
public async Task<ActionResult<PagedData<TemplateDto>>> GetAsync(
|
||||
[FromQuery] int startIndex,
|
||||
[FromQuery] int length,
|
||||
[FromQuery] FilterOptions? filterOptions
|
||||
)
|
||||
{
|
||||
// Validation
|
||||
if (startIndex < 0)
|
||||
return Problem("Invalid start index specified", statusCode: 400);
|
||||
|
||||
if (length is < 1 or > 100)
|
||||
return Problem("Invalid length specified");
|
||||
|
||||
// Query building
|
||||
|
||||
var query = DatabaseRepository
|
||||
.Query();
|
||||
|
||||
// Filters
|
||||
if (filterOptions != null)
|
||||
{
|
||||
foreach (var filterOption in filterOptions.Filters)
|
||||
{
|
||||
query = filterOption.Key switch
|
||||
{
|
||||
nameof(Template.Name) =>
|
||||
query.Where(role => EF.Functions.ILike(role.Name, $"%{filterOption.Value}%")),
|
||||
|
||||
_ => query
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination
|
||||
var data = await query
|
||||
.OrderBy(x => x.Id)
|
||||
.ProjectToDto()
|
||||
.Skip(startIndex)
|
||||
.Take(length)
|
||||
.ToArrayAsync();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
return new PagedData<TemplateDto>(data, total);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.View)]
|
||||
public async Task<ActionResult<DetailedTemplateDto>> GetAsync([FromRoute] int id)
|
||||
{
|
||||
var template = await DatabaseRepository
|
||||
.Query()
|
||||
.Include(x => x.DefaultDockerImage)
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (template == null)
|
||||
return Problem("No template with this id found", statusCode: 404);
|
||||
|
||||
return TemplateMapper.ToDetailedDto(template);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = Permissions.Templates.Create)]
|
||||
public async Task<ActionResult<TemplateDto>> CreateAsync([FromBody] CreateTemplateDto request)
|
||||
{
|
||||
var template = TemplateMapper.ToEntity(request);
|
||||
|
||||
// Fill in default values
|
||||
template.LifecycleConfig = new()
|
||||
{
|
||||
StartupCommands = [
|
||||
new StartupCommand
|
||||
{
|
||||
DisplayName = "Default Startup",
|
||||
Command = "bash startup.sh"
|
||||
}
|
||||
],
|
||||
StopCommand = "^C",
|
||||
OnlineLogPatterns = ["I am online"]
|
||||
};
|
||||
|
||||
template.InstallationConfig = new()
|
||||
{
|
||||
DockerImage = "debian",
|
||||
Script = "#!/bin/bash\necho Installing",
|
||||
Shell = "/bin/bash"
|
||||
};
|
||||
|
||||
template.FilesConfig = new()
|
||||
{
|
||||
ConfigurationFiles = []
|
||||
};
|
||||
|
||||
template.MiscellaneousConfig = new()
|
||||
{
|
||||
UseLegacyStartup = true
|
||||
};
|
||||
|
||||
var finalRole = await DatabaseRepository.AddAsync(template);
|
||||
|
||||
return TemplateMapper.ToDto(finalRole);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.Edit)]
|
||||
public async Task<ActionResult<DetailedTemplateDto>> UpdateAsync([FromRoute] int id, [FromBody] UpdateTemplateDto request)
|
||||
{
|
||||
var template = await DatabaseRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (template == null)
|
||||
return Problem("No template with this id found", statusCode: 404);
|
||||
|
||||
TemplateMapper.Merge(template, request);
|
||||
|
||||
template.DefaultDockerImage = await DockerImageRepository
|
||||
.Query()
|
||||
.Where(x => x.Template.Id == id)
|
||||
.FirstOrDefaultAsync(x => x.Id == request.DefaultDockerImageId);
|
||||
|
||||
await DatabaseRepository.UpdateAsync(template);
|
||||
|
||||
return TemplateMapper.ToDetailedDto(template);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.Delete)]
|
||||
public async Task<ActionResult> DeleteAsync([FromRoute] int id)
|
||||
{
|
||||
var template = await DatabaseRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (template == null)
|
||||
return Problem("No template with this id found", statusCode: 404);
|
||||
|
||||
await DatabaseRepository.RemoveAsync(template);
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
144
MoonlightServers.Api/Admin/Templates/DockerImagesController.cs
Normal file
144
MoonlightServers.Api/Admin/Templates/DockerImagesController.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/servers/templates/{templateId:int}/dockerImages")]
|
||||
public class DockerImagesController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||
|
||||
public DockerImagesController(
|
||||
DatabaseRepository<TemplateDockerImage> dockerImageRepository,
|
||||
DatabaseRepository<Template> templateRepository
|
||||
)
|
||||
{
|
||||
DockerImageRepository = dockerImageRepository;
|
||||
TemplateRepository = templateRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Permissions.Templates.View)]
|
||||
public async Task<ActionResult<PagedData<DockerImageDto>>> GetAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromQuery] int startIndex,
|
||||
[FromQuery] int length
|
||||
)
|
||||
{
|
||||
// Validation
|
||||
if (startIndex < 0)
|
||||
return Problem("Invalid start index specified", statusCode: 400);
|
||||
|
||||
if (length is < 1 or > 100)
|
||||
return Problem("Invalid length specified");
|
||||
|
||||
if (!await TemplateRepository.Query().AnyAsync(x => x.Id == templateId))
|
||||
return Problem("No template with that id found", statusCode: 404);
|
||||
|
||||
// Query building
|
||||
|
||||
var query = DockerImageRepository
|
||||
.Query()
|
||||
.Where(x => x.Template.Id == templateId);
|
||||
|
||||
// Pagination
|
||||
var data = await query
|
||||
.OrderBy(x => x.Id)
|
||||
.ProjectToDto()
|
||||
.Skip(startIndex)
|
||||
.Take(length)
|
||||
.ToArrayAsync();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
return new PagedData<DockerImageDto>(data, total);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.View)]
|
||||
public async Task<ActionResult<DockerImageDto>> GetAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromRoute] int id
|
||||
)
|
||||
{
|
||||
var templateDockerImage = await DockerImageRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||
|
||||
if (templateDockerImage == null)
|
||||
return Problem("No template or template dockerImage found with that id");
|
||||
|
||||
return TemplateMapper.ToDto(templateDockerImage);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = Permissions.Templates.Create)]
|
||||
public async Task<ActionResult<DockerImageDto>> CreateAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromBody] CreateDockerImageDto dto
|
||||
)
|
||||
{
|
||||
var template = await TemplateRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == templateId);
|
||||
|
||||
if (template == null)
|
||||
return Problem("No template with that id found", statusCode: 404);
|
||||
|
||||
var dockerImage = TemplateMapper.ToEntity(dto);
|
||||
|
||||
dockerImage.Template = template;
|
||||
|
||||
var finalDockerImage = await DockerImageRepository.AddAsync(dockerImage);
|
||||
|
||||
return TemplateMapper.ToDto(finalDockerImage);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.Edit)]
|
||||
public async Task<ActionResult<DockerImageDto>> UpdateAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromRoute] int id,
|
||||
[FromBody] UpdateDockerImageDto dto
|
||||
)
|
||||
{
|
||||
var templateDockerImage = await DockerImageRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||
|
||||
if (templateDockerImage == null)
|
||||
return Problem("No template or template dockerImage found with that id");
|
||||
|
||||
TemplateMapper.Merge(templateDockerImage, dto);
|
||||
|
||||
await DockerImageRepository.UpdateAsync(templateDockerImage);
|
||||
|
||||
return TemplateMapper.ToDto(templateDockerImage);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.Delete)]
|
||||
public async Task<ActionResult> DeleteAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromRoute] int id
|
||||
)
|
||||
{
|
||||
var templateDockerImage = await DockerImageRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||
|
||||
if (templateDockerImage == null)
|
||||
return Problem("No template or template dockerImage found with that id");
|
||||
|
||||
await DockerImageRepository.RemoveAsync(templateDockerImage);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
207
MoonlightServers.Api/Admin/Templates/PelicanEggImportService.cs
Normal file
207
MoonlightServers.Api/Admin/Templates/PelicanEggImportService.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using System.Text;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
using VYaml.Annotations;
|
||||
using VYaml.Serialization;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
public class PelicanEggImportService
|
||||
{
|
||||
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||
|
||||
public PelicanEggImportService(
|
||||
DatabaseRepository<Template> templateRepository,
|
||||
DatabaseRepository<TemplateDockerImage> dockerImageRepository
|
||||
)
|
||||
{
|
||||
TemplateRepository = templateRepository;
|
||||
DockerImageRepository = dockerImageRepository;
|
||||
}
|
||||
|
||||
public async Task<Template> ImportAsync(string content)
|
||||
{
|
||||
var egg = YamlSerializer.Deserialize<Egg>(Encoding.UTF8.GetBytes(content));
|
||||
|
||||
var template = new Template()
|
||||
{
|
||||
AllowUserDockerImageChange = true,
|
||||
Author = egg.Author,
|
||||
Description = egg.Description,
|
||||
DonateUrl = null,
|
||||
Name = egg.Name,
|
||||
UpdateUrl = egg.Meta.UpdateUrl,
|
||||
Version = "1.0.0",
|
||||
FilesConfig = new FilesConfig()
|
||||
{
|
||||
ConfigurationFiles = egg.Config.Files.Select(file => new ConfigurationFile()
|
||||
{
|
||||
Path = file.Key,
|
||||
Parser = file.Value.Parser,
|
||||
Mappings = file.Value.Find.Select(pair => new ConfigurationFileMapping()
|
||||
{
|
||||
Key = pair.Key,
|
||||
Value = pair.Value
|
||||
}).ToList()
|
||||
}).ToList()
|
||||
},
|
||||
InstallationConfig = new InstallationConfig()
|
||||
{
|
||||
DockerImage = egg.Scripts.Installation.Container,
|
||||
Script = egg.Scripts.Installation.Script,
|
||||
Shell = egg.Scripts.Installation.Entrypoint
|
||||
},
|
||||
LifecycleConfig = new LifecycleConfig()
|
||||
{
|
||||
OnlineLogPatterns = egg.Config.Startup.Values.ToList(),
|
||||
StopCommand = egg.Config.Stop,
|
||||
StartupCommands = egg.StartupCommands.Select(x => new StartupCommand()
|
||||
{
|
||||
DisplayName = x.Key,
|
||||
Command = x.Value
|
||||
}).ToList()
|
||||
},
|
||||
MiscellaneousConfig = new MiscellaneousConfig()
|
||||
{
|
||||
UseLegacyStartup = true
|
||||
},
|
||||
Variables = egg.Variables.Select(variable => new TemplateVariable()
|
||||
{
|
||||
Description = variable.Description,
|
||||
DisplayName = variable.Name,
|
||||
DefaultValue = variable.DefaultValue,
|
||||
EnvName = variable.EnvVariable
|
||||
}).ToList()
|
||||
};
|
||||
|
||||
var finalTemplate = await TemplateRepository.AddAsync(template);
|
||||
|
||||
var isFirst = true;
|
||||
TemplateDockerImage? defaultDockerImage = null;
|
||||
|
||||
foreach (var dockerImage in egg.DockerImages)
|
||||
{
|
||||
var finalDockerImage = await DockerImageRepository.AddAsync(new TemplateDockerImage()
|
||||
{
|
||||
DisplayName = dockerImage.Key,
|
||||
ImageName = dockerImage.Value,
|
||||
SkipPulling = false,
|
||||
Template = finalTemplate
|
||||
});
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
defaultDockerImage = finalDockerImage;
|
||||
}
|
||||
}
|
||||
|
||||
finalTemplate.DefaultDockerImage = defaultDockerImage;
|
||||
|
||||
await TemplateRepository.UpdateAsync(finalTemplate);
|
||||
|
||||
return finalTemplate;
|
||||
}
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class Egg
|
||||
{
|
||||
[YamlMember("_comment")] public string? Comment { get; set; }
|
||||
|
||||
[YamlMember("meta")] public EggMeta Meta { get; set; } = new();
|
||||
|
||||
[YamlMember("exported_at")] public string? ExportedAt { get; set; }
|
||||
|
||||
[YamlMember("name")] public string Name { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("author")] public string Author { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("uuid")] public string Uuid { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("description")] public string Description { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("image")] public string? Image { get; set; }
|
||||
|
||||
[YamlMember("tags")] public List<string> Tags { get; set; } = new();
|
||||
|
||||
[YamlMember("features")] public List<string> Features { get; set; } = new();
|
||||
|
||||
[YamlMember("docker_images")] public Dictionary<string, string> DockerImages { get; set; } = new();
|
||||
|
||||
[YamlMember("file_denylist")] public Dictionary<string, string> FileDenylist { get; set; } = new();
|
||||
|
||||
[YamlMember("startup_commands")] public Dictionary<string, string> StartupCommands { get; set; } = new();
|
||||
|
||||
[YamlMember("config")] public EggConfig Config { get; set; } = new();
|
||||
|
||||
[YamlMember("scripts")] public EggScripts Scripts { get; set; } = new();
|
||||
|
||||
[YamlMember("variables")] public List<EggVariable> Variables { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class EggMeta
|
||||
{
|
||||
[YamlMember("version")] public string Version { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("update_url")] public string? UpdateUrl { get; set; }
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class EggConfig
|
||||
{
|
||||
[YamlMember("files")] public Dictionary<string, EggConfigFile> Files { get; set; } = new();
|
||||
|
||||
[YamlMember("startup")] public Dictionary<string, string> Startup { get; set; } = new();
|
||||
|
||||
[YamlMember("logs")] public Dictionary<string, string> Logs { get; set; } = new();
|
||||
|
||||
[YamlMember("stop")] public string Stop { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class EggConfigFile
|
||||
{
|
||||
[YamlMember("parser")] public string Parser { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("find")] public Dictionary<string, string> Find { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class EggScripts
|
||||
{
|
||||
[YamlMember("installation")] public EggInstallationScript Installation { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class EggInstallationScript
|
||||
{
|
||||
[YamlMember("script")] public string Script { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("container")] public string Container { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("entrypoint")] public string Entrypoint { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class EggVariable
|
||||
{
|
||||
[YamlMember("name")] public string Name { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("description")] public string Description { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("env_variable")] public string EnvVariable { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("default_value")] public string DefaultValue { get; set; } = string.Empty;
|
||||
|
||||
[YamlMember("user_viewable")] public bool UserViewable { get; set; }
|
||||
|
||||
[YamlMember("user_editable")] public bool UserEditable { get; set; }
|
||||
|
||||
[YamlMember("rules")] public List<string> Rules { get; set; } = new();
|
||||
|
||||
[YamlMember("sort")] public int Sort { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
using System.Text.Json;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
public class PterodactylEggImportService
|
||||
{
|
||||
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||
|
||||
public PterodactylEggImportService(
|
||||
DatabaseRepository<Template> templateRepository,
|
||||
DatabaseRepository<TemplateDockerImage> dockerImageRepository
|
||||
)
|
||||
{
|
||||
TemplateRepository = templateRepository;
|
||||
DockerImageRepository = dockerImageRepository;
|
||||
}
|
||||
|
||||
public async Task<Template> ImportAsync(string json)
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
var template = new Template
|
||||
{
|
||||
Name = Truncate(root.GetStringOrDefault("name") ?? "Unknown", 30),
|
||||
Description = Truncate(root.GetStringOrDefault("description") ?? "", 255),
|
||||
Author = Truncate(root.GetStringOrDefault("author") ?? "", 30),
|
||||
Version = "1.0.0",
|
||||
UpdateUrl = root.TryGetProperty("meta", out var meta)
|
||||
? meta.GetStringOrDefault("update_url")
|
||||
: null,
|
||||
DonateUrl = null,
|
||||
|
||||
FilesConfig = ParseFilesConfig(root),
|
||||
LifecycleConfig = ParseLifecycleConfig(root),
|
||||
InstallationConfig = ParseInstallationConfig(root),
|
||||
MiscellaneousConfig = new MiscellaneousConfig { UseLegacyStartup = true },
|
||||
|
||||
AllowUserDockerImageChange = true,
|
||||
Variables = ParseVariables(root)
|
||||
};
|
||||
|
||||
var finalTemplate = await TemplateRepository.AddAsync(template);
|
||||
|
||||
var dockerImages = ParseDockerImageModels(root);
|
||||
TemplateDockerImage? defaultDockerImage = null;
|
||||
|
||||
var isFirst = true;
|
||||
|
||||
foreach (var (displayName, imageName) in dockerImages)
|
||||
{
|
||||
var entity = new TemplateDockerImage
|
||||
{
|
||||
DisplayName = displayName,
|
||||
ImageName = imageName,
|
||||
SkipPulling = false,
|
||||
Template = finalTemplate
|
||||
};
|
||||
|
||||
var finalEntity = await DockerImageRepository.AddAsync(entity);
|
||||
|
||||
if (isFirst)
|
||||
{
|
||||
isFirst = false;
|
||||
defaultDockerImage = finalEntity;
|
||||
}
|
||||
}
|
||||
|
||||
finalTemplate.DefaultDockerImage = defaultDockerImage;
|
||||
await TemplateRepository.UpdateAsync(finalTemplate);
|
||||
|
||||
return finalTemplate;
|
||||
}
|
||||
|
||||
private static FilesConfig ParseFilesConfig(JsonElement root)
|
||||
{
|
||||
var configFiles = new List<ConfigurationFile>();
|
||||
|
||||
if (!root.TryGetProperty("config", out var config))
|
||||
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||
|
||||
if (!config.TryGetProperty("files", out var filesElement))
|
||||
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||
|
||||
var filesJson = filesElement.ValueKind == JsonValueKind.String
|
||||
? filesElement.GetString()
|
||||
: filesElement.GetRawText();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(filesJson) || filesJson == "{}" || filesJson == "[]")
|
||||
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||
|
||||
try
|
||||
{
|
||||
using var filesDoc = JsonDocument.Parse(filesJson);
|
||||
|
||||
foreach (var fileProperty in filesDoc.RootElement.EnumerateObject())
|
||||
{
|
||||
var parser = fileProperty.Value.GetStringOrDefault("parser") ?? "json";
|
||||
var mappings = new List<ConfigurationFileMapping>();
|
||||
|
||||
if (fileProperty.Value.TryGetProperty("find", out var find))
|
||||
{
|
||||
foreach (var mapping in find.EnumerateObject())
|
||||
{
|
||||
mappings.Add(new ConfigurationFileMapping
|
||||
{
|
||||
Key = mapping.Name,
|
||||
Value = mapping.Value.ValueKind == JsonValueKind.String
|
||||
? mapping.Value.GetString() ?? ""
|
||||
: mapping.Value.GetRawText()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
configFiles.Add(new ConfigurationFile
|
||||
{
|
||||
Path = fileProperty.Name,
|
||||
Parser = parser,
|
||||
Mappings = mappings
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
return new FilesConfig { ConfigurationFiles = configFiles };
|
||||
}
|
||||
|
||||
private static LifecycleConfig ParseLifecycleConfig(JsonElement root)
|
||||
{
|
||||
var stopCommand = "";
|
||||
var onlinePatterns = new List<string>();
|
||||
|
||||
if (root.TryGetProperty("config", out var config))
|
||||
{
|
||||
stopCommand = config.GetStringOrDefault("stop") ?? "";
|
||||
|
||||
if (config.TryGetProperty("startup", out var startupElement))
|
||||
{
|
||||
var startupJson = startupElement.ValueKind == JsonValueKind.String
|
||||
? startupElement.GetString()
|
||||
: startupElement.GetRawText();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(startupJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
using var startupDoc = JsonDocument.Parse(startupJson);
|
||||
|
||||
if (startupDoc.RootElement.TryGetProperty("done", out var done))
|
||||
{
|
||||
var doneValue = done.ValueKind == JsonValueKind.String
|
||||
? done.GetString()
|
||||
: done.GetRawText();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(doneValue))
|
||||
onlinePatterns.Add(doneValue);
|
||||
}
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new LifecycleConfig
|
||||
{
|
||||
StartupCommands =
|
||||
[
|
||||
new StartupCommand
|
||||
{
|
||||
DisplayName = "Startup",
|
||||
Command = root.GetStringOrDefault("startup") ?? ""
|
||||
}
|
||||
],
|
||||
StopCommand = stopCommand,
|
||||
OnlineLogPatterns = onlinePatterns
|
||||
};
|
||||
}
|
||||
|
||||
private static InstallationConfig ParseInstallationConfig(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("scripts", out var scripts))
|
||||
return new InstallationConfig();
|
||||
|
||||
if (!scripts.TryGetProperty("installation", out var installation))
|
||||
return new InstallationConfig();
|
||||
|
||||
return new InstallationConfig
|
||||
{
|
||||
DockerImage = installation.GetStringOrDefault("container") ?? "",
|
||||
Shell = installation.GetStringOrDefault("entrypoint") ?? "bash",
|
||||
Script = installation.GetStringOrDefault("script") ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
// Returns (DisplayName, ImageName, IsFirst) tuples to avoid a temporary model
|
||||
private static List<(string DisplayName, string ImageName)> ParseDockerImageModels(JsonElement root)
|
||||
{
|
||||
var result = new List<(string, string)>();
|
||||
|
||||
if (!root.TryGetProperty("docker_images", out var dockerImages))
|
||||
return result;
|
||||
|
||||
foreach (var img in dockerImages.EnumerateObject())
|
||||
{
|
||||
result.Add((
|
||||
Truncate(img.Name, 30),
|
||||
Truncate(img.Value.GetString() ?? img.Name, 255)
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<TemplateVariable> ParseVariables(JsonElement root)
|
||||
{
|
||||
var variables = new List<TemplateVariable>();
|
||||
|
||||
if (!root.TryGetProperty("variables", out var vars))
|
||||
return variables;
|
||||
|
||||
foreach (var v in vars.EnumerateArray())
|
||||
{
|
||||
variables.Add(new TemplateVariable
|
||||
{
|
||||
DisplayName = Truncate(v.GetStringOrDefault("name") ?? "Variable", 30),
|
||||
Description = Truncate(v.GetStringOrDefault("description") ?? "", 255),
|
||||
EnvName = Truncate(v.GetStringOrDefault("env_variable") ?? "", 60),
|
||||
DefaultValue = Truncate(v.GetStringOrDefault("default_value") ?? "", 1024)
|
||||
});
|
||||
}
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
private static string Truncate(string value, int maxLength) =>
|
||||
value.Length <= maxLength ? value : value[..maxLength];
|
||||
}
|
||||
|
||||
internal static class JsonElementExtensions
|
||||
{
|
||||
public static string? GetStringOrDefault(this JsonElement element, string propertyName)
|
||||
{
|
||||
return element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String
|
||||
? prop.GetString()
|
||||
: null;
|
||||
}
|
||||
}
|
||||
28
MoonlightServers.Api/Admin/Templates/TemplateMapper.cs
Normal file
28
MoonlightServers.Api/Admin/Templates/TemplateMapper.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class TemplateMapper
|
||||
{
|
||||
public static partial TemplateDto ToDto(Template template);
|
||||
public static partial DetailedTemplateDto ToDetailedDto(Template template);
|
||||
public static partial IQueryable<TemplateDto> ProjectToDto(this IQueryable<Template> templates);
|
||||
public static partial Template ToEntity(CreateTemplateDto dto);
|
||||
public static partial void Merge([MappingTarget] Template template, UpdateTemplateDto dto);
|
||||
|
||||
public static partial IQueryable<VariableDto> ProjectToDto(this IQueryable<TemplateVariable> variables);
|
||||
public static partial VariableDto ToDto(TemplateVariable variable);
|
||||
public static partial TemplateVariable ToEntity(CreateVariableDto dto);
|
||||
public static partial void Merge([MappingTarget] TemplateVariable variable, UpdateVariableDto dto);
|
||||
|
||||
public static partial IQueryable<DockerImageDto> ProjectToDto(this IQueryable<TemplateDockerImage> dockerImages);
|
||||
public static partial DockerImageDto ToDto(TemplateDockerImage dockerImage);
|
||||
public static partial TemplateDockerImage ToEntity(CreateDockerImageDto dto);
|
||||
public static partial void Merge([MappingTarget] TemplateDockerImage dockerImage, UpdateDockerImageDto dto);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using VYaml.Annotations;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
[YamlObject]
|
||||
public partial class TemplateTransferModel
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string Author { get; set; } = "";
|
||||
public string Version { get; set; } = "";
|
||||
public string? UpdateUrl { get; set; }
|
||||
public string? DonateUrl { get; set; }
|
||||
|
||||
public FilesConfigTransferModel Files { get; set; } = new();
|
||||
public LifecycleConfigTransferModel Lifecycle { get; set; } = new();
|
||||
public InstallationConfigTransferModel Installation { get; set; } = new();
|
||||
public MiscellaneousConfigTransferModel Miscellaneous { get; set; } = new();
|
||||
|
||||
public bool AllowUserDockerImageChange { get; set; }
|
||||
public List<TemplateDockerImageTransferModel> DockerImages { get; set; } = new();
|
||||
|
||||
public List<TemplateVariableTransferModel> Variables { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class TemplateDockerImageTransferModel
|
||||
{
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string ImageName { get; set; } = "";
|
||||
public bool SkipPulling { get; set; }
|
||||
public bool IsDefault { get; set; }
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class TemplateVariableTransferModel
|
||||
{
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string Description { get; set; } = "";
|
||||
public string EnvName { get; set; } = "";
|
||||
public string? DefaultValue { get; set; }
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class FilesConfigTransferModel
|
||||
{
|
||||
public List<ConfigurationFileTransferModel> ConfigurationFiles { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class ConfigurationFileTransferModel
|
||||
{
|
||||
public string Path { get; set; } = "";
|
||||
public string Parser { get; set; } = "";
|
||||
public List<ConfigurationFileMappingTransferModel> Mappings { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class ConfigurationFileMappingTransferModel
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public string? Value { get; set; }
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class LifecycleConfigTransferModel
|
||||
{
|
||||
public List<StartupCommandTransferModel> StartupCommands { get; set; } = new();
|
||||
public string StopCommand { get; set; } = "";
|
||||
public List<string> OnlineLogPatterns { get; set; } = new();
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class StartupCommandTransferModel
|
||||
{
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string Command { get; set; } = "";
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class InstallationConfigTransferModel
|
||||
{
|
||||
public string DockerImage { get; set; } = "";
|
||||
public string Shell { get; set; } = "";
|
||||
public string Script { get; set; } = "";
|
||||
}
|
||||
|
||||
[YamlObject]
|
||||
public partial class MiscellaneousConfigTransferModel
|
||||
{
|
||||
public bool UseLegacyStartup { get; set; }
|
||||
}
|
||||
182
MoonlightServers.Api/Admin/Templates/TemplateTransferService.cs
Normal file
182
MoonlightServers.Api/Admin/Templates/TemplateTransferService.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
public class TemplateTransferService
|
||||
{
|
||||
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||
private readonly DatabaseRepository<TemplateDockerImage> DockerImageRepository;
|
||||
|
||||
public TemplateTransferService(DatabaseRepository<Template> templateRepository,
|
||||
DatabaseRepository<TemplateDockerImage> dockerImageRepository)
|
||||
{
|
||||
TemplateRepository = templateRepository;
|
||||
DockerImageRepository = dockerImageRepository;
|
||||
}
|
||||
|
||||
public async Task<TemplateTransferModel?> ExportAsync(int id)
|
||||
{
|
||||
var template = await TemplateRepository
|
||||
.Query()
|
||||
.Include(x => x.Variables)
|
||||
.Include(x => x.DockerImages)
|
||||
.Include(x => x.DefaultDockerImage)
|
||||
.FirstOrDefaultAsync(x => x.Id == id);
|
||||
|
||||
if (template == null)
|
||||
return null;
|
||||
|
||||
return new()
|
||||
{
|
||||
Name = template.Name,
|
||||
Description = template.Description,
|
||||
Author = template.Author,
|
||||
Version = template.Version,
|
||||
UpdateUrl = template.UpdateUrl,
|
||||
DonateUrl = template.DonateUrl,
|
||||
|
||||
Files = new FilesConfigTransferModel
|
||||
{
|
||||
ConfigurationFiles = template.FilesConfig.ConfigurationFiles
|
||||
.Select(cf => new ConfigurationFileTransferModel
|
||||
{
|
||||
Path = cf.Path,
|
||||
Parser = cf.Parser,
|
||||
Mappings = cf.Mappings
|
||||
.Select(m => new ConfigurationFileMappingTransferModel { Key = m.Key, Value = m.Value })
|
||||
.ToList()
|
||||
})
|
||||
.ToList()
|
||||
},
|
||||
|
||||
Lifecycle = new LifecycleConfigTransferModel
|
||||
{
|
||||
StartupCommands = template.LifecycleConfig.StartupCommands
|
||||
.Select(sc => new StartupCommandTransferModel
|
||||
{ DisplayName = sc.DisplayName, Command = sc.Command })
|
||||
.ToList(),
|
||||
StopCommand = template.LifecycleConfig.StopCommand,
|
||||
OnlineLogPatterns = template.LifecycleConfig.OnlineLogPatterns.ToList()
|
||||
},
|
||||
|
||||
Installation = new InstallationConfigTransferModel
|
||||
{
|
||||
DockerImage = template.InstallationConfig.DockerImage,
|
||||
Shell = template.InstallationConfig.Shell,
|
||||
Script = template.InstallationConfig.Script
|
||||
},
|
||||
|
||||
Miscellaneous = new MiscellaneousConfigTransferModel
|
||||
{
|
||||
UseLegacyStartup = template.MiscellaneousConfig.UseLegacyStartup
|
||||
},
|
||||
|
||||
AllowUserDockerImageChange = template.AllowUserDockerImageChange,
|
||||
DockerImages = template.DockerImages
|
||||
.Select(img => new TemplateDockerImageTransferModel
|
||||
{
|
||||
DisplayName = img.DisplayName,
|
||||
ImageName = img.ImageName,
|
||||
SkipPulling = img.SkipPulling,
|
||||
IsDefault = template.DefaultDockerImage != null && img.Id == template.DefaultDockerImage.Id
|
||||
})
|
||||
.ToList(),
|
||||
|
||||
Variables = template.Variables
|
||||
.Select(v => new TemplateVariableTransferModel
|
||||
{
|
||||
DisplayName = v.DisplayName,
|
||||
Description = v.Description,
|
||||
EnvName = v.EnvName,
|
||||
DefaultValue = v.DefaultValue
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<Template> ImportAsync(TemplateTransferModel m)
|
||||
{
|
||||
var template = new Template
|
||||
{
|
||||
Name = m.Name,
|
||||
Description = m.Description,
|
||||
Author = m.Author,
|
||||
Version = m.Version,
|
||||
UpdateUrl = m.UpdateUrl,
|
||||
DonateUrl = m.DonateUrl,
|
||||
|
||||
FilesConfig = new FilesConfig
|
||||
{
|
||||
ConfigurationFiles = m.Files.ConfigurationFiles
|
||||
.Select(cf => new ConfigurationFile
|
||||
{
|
||||
Path = cf.Path,
|
||||
Parser = cf.Parser,
|
||||
Mappings = cf.Mappings
|
||||
.Select(mp => new ConfigurationFileMapping { Key = mp.Key, Value = mp.Value })
|
||||
.ToList()
|
||||
})
|
||||
.ToList()
|
||||
},
|
||||
|
||||
LifecycleConfig = new LifecycleConfig
|
||||
{
|
||||
StartupCommands = m.Lifecycle.StartupCommands
|
||||
.Select(sc => new StartupCommand { DisplayName = sc.DisplayName, Command = sc.Command })
|
||||
.ToList(),
|
||||
StopCommand = m.Lifecycle.StopCommand,
|
||||
OnlineLogPatterns = m.Lifecycle.OnlineLogPatterns.ToList()
|
||||
},
|
||||
|
||||
InstallationConfig = new InstallationConfig
|
||||
{
|
||||
DockerImage = m.Installation.DockerImage,
|
||||
Shell = m.Installation.Shell,
|
||||
Script = m.Installation.Script
|
||||
},
|
||||
|
||||
MiscellaneousConfig = new MiscellaneousConfig { UseLegacyStartup = m.Miscellaneous.UseLegacyStartup },
|
||||
|
||||
AllowUserDockerImageChange = m.AllowUserDockerImageChange,
|
||||
|
||||
Variables = m.Variables
|
||||
.Select(v => new TemplateVariable
|
||||
{
|
||||
DisplayName = v.DisplayName,
|
||||
Description = v.Description,
|
||||
EnvName = v.EnvName,
|
||||
DefaultValue = v.DefaultValue
|
||||
})
|
||||
.ToList()
|
||||
};
|
||||
|
||||
var finalTemplate = await TemplateRepository.AddAsync(template);
|
||||
|
||||
TemplateDockerImage? defaultDockerImage = null;
|
||||
|
||||
foreach (var img in m.DockerImages)
|
||||
{
|
||||
var entity = new TemplateDockerImage
|
||||
{
|
||||
DisplayName = img.DisplayName,
|
||||
ImageName = img.ImageName,
|
||||
SkipPulling = img.SkipPulling,
|
||||
Template = template
|
||||
};
|
||||
|
||||
var finalEntity = await DockerImageRepository.AddAsync(entity);
|
||||
|
||||
if (img.IsDefault)
|
||||
defaultDockerImage = finalEntity;
|
||||
}
|
||||
|
||||
finalTemplate.DefaultDockerImage = defaultDockerImage;
|
||||
|
||||
await TemplateRepository.UpdateAsync(finalTemplate);
|
||||
|
||||
return finalTemplate;
|
||||
}
|
||||
}
|
||||
88
MoonlightServers.Api/Admin/Templates/TransferController.cs
Normal file
88
MoonlightServers.Api/Admin/Templates/TransferController.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Text;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
using VYaml.Serialization;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/servers/templates")]
|
||||
public class TransferController : Controller
|
||||
{
|
||||
private readonly TemplateTransferService TransferService;
|
||||
|
||||
public TransferController(TemplateTransferService transferService)
|
||||
{
|
||||
TransferService = transferService;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/export")]
|
||||
public async Task<ActionResult> ExportAsync([FromRoute] int id)
|
||||
{
|
||||
var transferModel = await TransferService.ExportAsync(id);
|
||||
|
||||
if (transferModel == null)
|
||||
return Problem("No template with that id found", statusCode: 404);
|
||||
|
||||
var yml = YamlSerializer.Serialize(transferModel, new YamlSerializerOptions
|
||||
{
|
||||
Resolver = CompositeResolver.Create([
|
||||
GeneratedResolver.Instance,
|
||||
StandardResolver.Instance
|
||||
])
|
||||
});
|
||||
|
||||
return File(yml.ToArray(), "text/yaml", $"{transferModel.Name}.yml");
|
||||
}
|
||||
|
||||
[HttpPost("import")]
|
||||
public async Task<ActionResult<TemplateDto>> ImportAsync()
|
||||
{
|
||||
string content;
|
||||
|
||||
await using (Stream receiveStream = Request.Body)
|
||||
|
||||
using (StreamReader readStream = new StreamReader(receiveStream))
|
||||
content = await readStream.ReadToEndAsync();
|
||||
|
||||
if(content.Contains("version: PLCN_v3"))
|
||||
{
|
||||
var importService = HttpContext.RequestServices.GetRequiredService<PelicanEggImportService>();
|
||||
|
||||
var template = await importService.ImportAsync(content);
|
||||
|
||||
return TemplateMapper.ToDto(template);
|
||||
}
|
||||
if (
|
||||
content.Contains("PTDL_v2", StringComparison.OrdinalIgnoreCase) ||
|
||||
content.Contains("PLCN_v1", StringComparison.OrdinalIgnoreCase) ||
|
||||
content.Contains("PLCN_v2", StringComparison.OrdinalIgnoreCase) ||
|
||||
content.Contains("PLCN_v3", StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
{
|
||||
var importService = HttpContext.RequestServices.GetRequiredService<PterodactylEggImportService>();
|
||||
|
||||
var template = await importService.ImportAsync(content);
|
||||
|
||||
return TemplateMapper.ToDto(template);
|
||||
}
|
||||
else
|
||||
{
|
||||
var transferModel = YamlSerializer.Deserialize<TemplateTransferModel>(
|
||||
Encoding.UTF8.GetBytes(content),
|
||||
new YamlSerializerOptions
|
||||
{
|
||||
Resolver = CompositeResolver.Create([
|
||||
GeneratedResolver.Instance,
|
||||
StandardResolver.Instance
|
||||
])
|
||||
}
|
||||
);
|
||||
|
||||
var template = await TransferService.ImportAsync(transferModel);
|
||||
|
||||
return TemplateMapper.ToDto(template);
|
||||
}
|
||||
}
|
||||
}
|
||||
144
MoonlightServers.Api/Admin/Templates/VariablesController.cs
Normal file
144
MoonlightServers.Api/Admin/Templates/VariablesController.cs
Normal file
@@ -0,0 +1,144 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Moonlight.Shared.Shared;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
using MoonlightServers.Shared;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
|
||||
namespace MoonlightServers.Api.Admin.Templates;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/servers/templates/{templateId:int}/variables")]
|
||||
public class VariablesController : Controller
|
||||
{
|
||||
private readonly DatabaseRepository<TemplateVariable> VariableRepository;
|
||||
private readonly DatabaseRepository<Template> TemplateRepository;
|
||||
|
||||
public VariablesController(
|
||||
DatabaseRepository<TemplateVariable> variableRepository,
|
||||
DatabaseRepository<Template> templateRepository
|
||||
)
|
||||
{
|
||||
VariableRepository = variableRepository;
|
||||
TemplateRepository = templateRepository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize(Policy = Permissions.Templates.View)]
|
||||
public async Task<ActionResult<PagedData<VariableDto>>> GetAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromQuery] int startIndex,
|
||||
[FromQuery] int length
|
||||
)
|
||||
{
|
||||
// Validation
|
||||
if (startIndex < 0)
|
||||
return Problem("Invalid start index specified", statusCode: 400);
|
||||
|
||||
if (length is < 1 or > 100)
|
||||
return Problem("Invalid length specified");
|
||||
|
||||
if (!await TemplateRepository.Query().AnyAsync(x => x.Id == templateId))
|
||||
return Problem("No template with that id found", statusCode: 404);
|
||||
|
||||
// Query building
|
||||
|
||||
var query = VariableRepository
|
||||
.Query()
|
||||
.Where(x => x.Template.Id == templateId);
|
||||
|
||||
// Pagination
|
||||
var data = await query
|
||||
.OrderBy(x => x.Id)
|
||||
.ProjectToDto()
|
||||
.Skip(startIndex)
|
||||
.Take(length)
|
||||
.ToArrayAsync();
|
||||
|
||||
var total = await query.CountAsync();
|
||||
|
||||
return new PagedData<VariableDto>(data, total);
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.View)]
|
||||
public async Task<ActionResult<VariableDto>> GetAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromRoute] int id
|
||||
)
|
||||
{
|
||||
var templateVariable = await VariableRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||
|
||||
if (templateVariable == null)
|
||||
return Problem("No template or template variable found with that id");
|
||||
|
||||
return TemplateMapper.ToDto(templateVariable);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[Authorize(Policy = Permissions.Templates.Create)]
|
||||
public async Task<ActionResult<VariableDto>> CreateAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromBody] CreateVariableDto dto
|
||||
)
|
||||
{
|
||||
var template = await TemplateRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == templateId);
|
||||
|
||||
if (template == null)
|
||||
return Problem("No template with that id found", statusCode: 404);
|
||||
|
||||
var variable = TemplateMapper.ToEntity(dto);
|
||||
|
||||
variable.Template = template;
|
||||
|
||||
var finalVariable = await VariableRepository.AddAsync(variable);
|
||||
|
||||
return TemplateMapper.ToDto(finalVariable);
|
||||
}
|
||||
|
||||
[HttpPut("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.Edit)]
|
||||
public async Task<ActionResult<VariableDto>> UpdateAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromRoute] int id,
|
||||
[FromBody] UpdateVariableDto dto
|
||||
)
|
||||
{
|
||||
var templateVariable = await VariableRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||
|
||||
if (templateVariable == null)
|
||||
return Problem("No template or template variable found with that id");
|
||||
|
||||
TemplateMapper.Merge(templateVariable, dto);
|
||||
|
||||
await VariableRepository.UpdateAsync(templateVariable);
|
||||
|
||||
return TemplateMapper.ToDto(templateVariable);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:int}")]
|
||||
[Authorize(Policy = Permissions.Templates.Delete)]
|
||||
public async Task<ActionResult> DeleteAsync(
|
||||
[FromRoute] int templateId,
|
||||
[FromRoute] int id
|
||||
)
|
||||
{
|
||||
var templateVariable = await VariableRepository
|
||||
.Query()
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Template.Id == templateId);
|
||||
|
||||
if (templateVariable == null)
|
||||
return Problem("No template or template variable found with that id");
|
||||
|
||||
await VariableRepository.RemoveAsync(templateVariable);
|
||||
return NoContent();
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonlightServers.Shared.Http.Requests;
|
||||
using MoonlightServers.Shared.Http.Responses;
|
||||
|
||||
namespace MoonlightServers.Api.Http.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/form")]
|
||||
public class FormController : Controller
|
||||
{
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<FormResultDto>> PostAsync([FromBody] FormSubmitDto dto)
|
||||
{
|
||||
return new FormResultDto()
|
||||
{
|
||||
Result = dto.TextField.Replace(" ", string.Empty)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MoonlightServers.Api.Infrastructure.Configuration;
|
||||
|
||||
public class NodeTokenOptions
|
||||
{
|
||||
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||
}
|
||||
66
MoonlightServers.Api/Infrastructure/Database/DataContext.cs
Normal file
66
MoonlightServers.Api/Infrastructure/Database/DataContext.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moonlight.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||
|
||||
public class DataContext : DbContext
|
||||
{
|
||||
public DbSet<Node> Nodes { get; set; }
|
||||
public DbSet<Template> Templates { get; set; }
|
||||
public DbSet<TemplateDockerImage> TemplateDockerImages { get; set; }
|
||||
public DbSet<TemplateVariable> TemplateVariablesVariables { get; set; }
|
||||
|
||||
private readonly IOptions<DatabaseOptions> Options;
|
||||
public DataContext(IOptions<DatabaseOptions> options)
|
||||
{
|
||||
Options = options;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
if (optionsBuilder.IsConfigured)
|
||||
return;
|
||||
|
||||
optionsBuilder.UseNpgsql(
|
||||
$"Host={Options.Value.Host};" +
|
||||
$"Port={Options.Value.Port};" +
|
||||
$"Username={Options.Value.Username};" +
|
||||
$"Password={Options.Value.Password};" +
|
||||
$"Database={Options.Value.Database}",
|
||||
builder =>
|
||||
{
|
||||
builder.MigrationsAssembly(typeof(DataContext).Assembly);
|
||||
builder.MigrationsHistoryTable("MigrationsHistory", "servers");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.HasDefaultSchema("servers");
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<Template>()
|
||||
.ComplexProperty(x => x.FilesConfig, builder => builder.ToJson())
|
||||
.ComplexProperty(x => x.LifecycleConfig, builder => builder.ToJson())
|
||||
.ComplexProperty(x => x.InstallationConfig, builder => builder.ToJson())
|
||||
.ComplexProperty(x => x.MiscellaneousConfig, builder => builder.ToJson());
|
||||
|
||||
// One-to-many: Template => DockerImages
|
||||
modelBuilder.Entity<Template>()
|
||||
.HasMany(t => t.DockerImages)
|
||||
.WithOne(d => d.Template)
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
// One-to-one: Template => DefaultDockerImage
|
||||
modelBuilder.Entity<Template>()
|
||||
.HasOne(t => t.DefaultDockerImage)
|
||||
.WithOne()
|
||||
.HasForeignKey<Template>("DefaultDockerImageId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Interfaces;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||
|
||||
public class DatabaseRepository<T> where T : class
|
||||
{
|
||||
private readonly DataContext DataContext;
|
||||
private readonly DbSet<T> Set;
|
||||
|
||||
public DatabaseRepository(DataContext dataContext)
|
||||
{
|
||||
DataContext = dataContext;
|
||||
Set = DataContext.Set<T>();
|
||||
}
|
||||
|
||||
public IQueryable<T> Query() => Set;
|
||||
|
||||
public async Task<T> AddAsync(T entity)
|
||||
{
|
||||
if (entity is IActionTimestamps actionTimestamps)
|
||||
{
|
||||
actionTimestamps.CreatedAt = DateTimeOffset.UtcNow;
|
||||
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
var final = Set.Add(entity);
|
||||
await DataContext.SaveChangesAsync();
|
||||
return final.Entity;
|
||||
}
|
||||
|
||||
public async Task UpdateAsync(T entity)
|
||||
{
|
||||
if (entity is IActionTimestamps actionTimestamps)
|
||||
actionTimestamps.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
Set.Update(entity);
|
||||
await DataContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task RemoveAsync(T entity)
|
||||
{
|
||||
Set.Remove(entity);
|
||||
await DataContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database;
|
||||
|
||||
public class DbMigrationService : IHostedLifecycleService
|
||||
{
|
||||
private readonly ILogger<DbMigrationService> Logger;
|
||||
private readonly IServiceProvider ServiceProvider;
|
||||
|
||||
public DbMigrationService(ILogger<DbMigrationService> logger, IServiceProvider serviceProvider)
|
||||
{
|
||||
Logger = logger;
|
||||
ServiceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task StartingAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Logger.LogTrace("Checking for pending migrations");
|
||||
|
||||
await using var scope = ServiceProvider.CreateAsyncScope();
|
||||
var context = scope.ServiceProvider.GetRequiredService<DataContext>();
|
||||
|
||||
var availableMigrations = context.Database.GetMigrations();
|
||||
Logger.LogTrace("Available migrations: {names}", string.Join(", ", availableMigrations));
|
||||
|
||||
var pendingMigrations = await context.Database.GetPendingMigrationsAsync(cancellationToken);
|
||||
var migrationNames = pendingMigrations.ToArray();
|
||||
|
||||
if (migrationNames.Length == 0)
|
||||
{
|
||||
Logger.LogTrace("No pending migrations found");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Pending migrations: {names}", string.Join(", ", migrationNames));
|
||||
Logger.LogInformation("Migration started");
|
||||
|
||||
await context.Database.MigrateAsync(cancellationToken);
|
||||
|
||||
Logger.LogInformation("Migration complete");
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StartedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppedAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
public Task StoppingAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Interfaces;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
public class Node : IActionTimestamps
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[MaxLength(50)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(100)]
|
||||
public string HttpEndpointUrl { get; set; }
|
||||
|
||||
[MaxLength(10)]
|
||||
public string TokenId { get; set; }
|
||||
|
||||
[MaxLength(64)]
|
||||
public string Token { get; set; }
|
||||
|
||||
// Action timestamps
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
public class Template
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
// Meta
|
||||
|
||||
[MaxLength(30)]
|
||||
public string Name { get; set; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[MaxLength(30)]
|
||||
public string Author { get; set; }
|
||||
|
||||
[MaxLength(30)]
|
||||
public string Version { get; set; }
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string? UpdateUrl { get; set; }
|
||||
|
||||
[MaxLength(2048)]
|
||||
public string? DonateUrl { get; set; }
|
||||
|
||||
// JSON Options
|
||||
public FilesConfig FilesConfig { get; set; }
|
||||
public LifecycleConfig LifecycleConfig { get; set; }
|
||||
public InstallationConfig InstallationConfig { get; set; }
|
||||
public MiscellaneousConfig MiscellaneousConfig { get; set; }
|
||||
|
||||
// Docker Images
|
||||
public bool AllowUserDockerImageChange { get; set; }
|
||||
public TemplateDockerImage? DefaultDockerImage { get; set; }
|
||||
public List<TemplateDockerImage> DockerImages { get; set; } = new();
|
||||
|
||||
// Variables
|
||||
public List<TemplateVariable> Variables { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
public class TemplateDockerImage
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[MaxLength(30)]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string ImageName { get; set; }
|
||||
|
||||
public bool SkipPulling { get; set; }
|
||||
|
||||
// Relations
|
||||
public Template Template { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
public class TemplateVariable
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[MaxLength(30)]
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
[MaxLength(255)]
|
||||
public string Description { get; set; }
|
||||
|
||||
[MaxLength(60)]
|
||||
public string EnvName { get; set; }
|
||||
|
||||
[MaxLength(1024)]
|
||||
public string? DefaultValue { get; set; }
|
||||
|
||||
// Relations
|
||||
public Template Template { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Interfaces;
|
||||
|
||||
internal interface IActionTimestamps
|
||||
{
|
||||
public DateTimeOffset CreatedAt { get; set; }
|
||||
public DateTimeOffset UpdatedAt { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
public class FilesConfig
|
||||
{
|
||||
public List<ConfigurationFile> ConfigurationFiles { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ConfigurationFile
|
||||
{
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string Parser { get; set; } = string.Empty;
|
||||
public List<ConfigurationFileMapping> Mappings { get; set; } = [];
|
||||
}
|
||||
|
||||
public class ConfigurationFileMapping
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string? Value { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
public class InstallationConfig
|
||||
{
|
||||
public string DockerImage { get; set; } = string.Empty;
|
||||
public string Shell { get; set; } = string.Empty;
|
||||
public string Script { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
public class LifecycleConfig
|
||||
{
|
||||
public List<StartupCommand> StartupCommands { get; set; } = [];
|
||||
public string StopCommand { get; set; } = string.Empty;
|
||||
public List<string> OnlineLogPatterns { get; set; } = [];
|
||||
}
|
||||
|
||||
public class StartupCommand
|
||||
{
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string Command { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Json;
|
||||
|
||||
public class MiscellaneousConfig
|
||||
{
|
||||
public bool UseLegacyStartup { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20260305104238_AddedBasicNodeEntity")]
|
||||
partial class AddedBasicNodeEntity
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("servers")
|
||||
.HasAnnotation("ProductVersion", "10.0.1")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("HttpEndpointUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Nodes", "servers");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedBasicNodeEntity : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "servers");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Nodes",
|
||||
schema: "servers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
HttpEndpointUrl = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
TokenId = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false),
|
||||
Token = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Nodes", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Nodes",
|
||||
schema: "servers");
|
||||
}
|
||||
}
|
||||
}
|
||||
315
MoonlightServers.Api/Infrastructure/Database/Migrations/20260312075719_AddedTemplateEntities.Designer.cs
generated
Normal file
315
MoonlightServers.Api/Infrastructure/Database/Migrations/20260312075719_AddedTemplateEntities.Designer.cs
generated
Normal file
@@ -0,0 +1,315 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20260312075719_AddedTemplateEntities")]
|
||||
partial class AddedTemplateEntities
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("servers")
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("HttpEndpointUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Nodes", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowUserDockerImageChange")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int?>("DefaultDockerImageId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DonateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("UpdateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Parser")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("Path")
|
||||
.IsRequired();
|
||||
|
||||
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
|
||||
{
|
||||
b3.IsRequired();
|
||||
|
||||
b3.Property<string>("Key")
|
||||
.IsRequired();
|
||||
|
||||
b3.Property<string>("Value")
|
||||
.IsRequired();
|
||||
});
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("FilesConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("DockerImage")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Script")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Shell")
|
||||
.IsRequired();
|
||||
|
||||
b1
|
||||
.ToJson("InstallationConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.PrimitiveCollection<string>("OnlineLogPatterns")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("StopCommand")
|
||||
.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Command")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("DisplayName")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("LifecycleConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("UseLegacyStartup");
|
||||
|
||||
b1
|
||||
.ToJson("MiscellaneousConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefaultDockerImageId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Templates", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("SkipPulling")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateDockerImages", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("EnvName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateVariablesVariables", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
|
||||
.WithOne()
|
||||
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DefaultDockerImage");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("DockerImages")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("Variables")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Navigation("DockerImages");
|
||||
|
||||
b.Navigation("Variables");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedTemplateEntities : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TemplateDockerImages",
|
||||
schema: "servers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DisplayName = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
ImageName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
SkipPulling = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TemplateId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TemplateDockerImages", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Templates",
|
||||
schema: "servers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
Author = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
Version = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
UpdateUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
DonateUrl = table.Column<string>(type: "character varying(2048)", maxLength: 2048, nullable: true),
|
||||
AllowUserDockerImageChange = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DefaultDockerImageId = table.Column<int>(type: "integer", nullable: true),
|
||||
FilesConfig = table.Column<string>(type: "jsonb", nullable: false),
|
||||
InstallationConfig = table.Column<string>(type: "jsonb", nullable: false),
|
||||
LifecycleConfig = table.Column<string>(type: "jsonb", nullable: false),
|
||||
MiscellaneousConfig = table.Column<string>(type: "jsonb", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Templates", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Templates_TemplateDockerImages_DefaultDockerImageId",
|
||||
column: x => x.DefaultDockerImageId,
|
||||
principalSchema: "servers",
|
||||
principalTable: "TemplateDockerImages",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TemplateVariablesVariables",
|
||||
schema: "servers",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
DisplayName = table.Column<string>(type: "character varying(30)", maxLength: 30, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
EnvName = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
|
||||
DefaultValue = table.Column<string>(type: "character varying(1024)", maxLength: 1024, nullable: false),
|
||||
TemplateId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TemplateVariablesVariables", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_TemplateVariablesVariables_Templates_TemplateId",
|
||||
column: x => x.TemplateId,
|
||||
principalSchema: "servers",
|
||||
principalTable: "Templates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateDockerImages_TemplateId",
|
||||
schema: "servers",
|
||||
table: "TemplateDockerImages",
|
||||
column: "TemplateId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Templates_DefaultDockerImageId",
|
||||
schema: "servers",
|
||||
table: "Templates",
|
||||
column: "DefaultDockerImageId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TemplateVariablesVariables_TemplateId",
|
||||
schema: "servers",
|
||||
table: "TemplateVariablesVariables",
|
||||
column: "TemplateId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_TemplateDockerImages_Templates_TemplateId",
|
||||
schema: "servers",
|
||||
table: "TemplateDockerImages",
|
||||
column: "TemplateId",
|
||||
principalSchema: "servers",
|
||||
principalTable: "Templates",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_TemplateDockerImages_Templates_TemplateId",
|
||||
schema: "servers",
|
||||
table: "TemplateDockerImages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TemplateVariablesVariables",
|
||||
schema: "servers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Templates",
|
||||
schema: "servers");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TemplateDockerImages",
|
||||
schema: "servers");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
[Migration("20260312153948_AddedNullabilityForTemplateVariableDefaultValue")]
|
||||
partial class AddedNullabilityForTemplateVariableDefaultValue
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("servers")
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("HttpEndpointUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Nodes", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowUserDockerImageChange")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int?>("DefaultDockerImageId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DonateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("UpdateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Parser")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("Path")
|
||||
.IsRequired();
|
||||
|
||||
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
|
||||
{
|
||||
b3.IsRequired();
|
||||
|
||||
b3.Property<string>("Key")
|
||||
.IsRequired();
|
||||
|
||||
b3.Property<string>("Value");
|
||||
});
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("FilesConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("DockerImage")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Script")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Shell")
|
||||
.IsRequired();
|
||||
|
||||
b1
|
||||
.ToJson("InstallationConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.PrimitiveCollection<string>("OnlineLogPatterns")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("StopCommand")
|
||||
.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Command")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("DisplayName")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("LifecycleConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("UseLegacyStartup");
|
||||
|
||||
b1
|
||||
.ToJson("MiscellaneousConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefaultDockerImageId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Templates", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("SkipPulling")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateDockerImages", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("EnvName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateVariablesVariables", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
|
||||
.WithOne()
|
||||
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DefaultDockerImage");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("DockerImages")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("Variables")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Navigation("DockerImages");
|
||||
|
||||
b.Navigation("Variables");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddedNullabilityForTemplateVariableDefaultValue : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DefaultValue",
|
||||
schema: "servers",
|
||||
table: "TemplateVariablesVariables",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "DefaultValue",
|
||||
schema: "servers",
|
||||
table: "TemplateVariablesVariables",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: false,
|
||||
defaultValue: "",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(1024)",
|
||||
oldMaxLength: 1024,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Database.Migrations
|
||||
{
|
||||
[DbContext(typeof(DataContext))]
|
||||
partial class DataContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasDefaultSchema("servers")
|
||||
.HasAnnotation("ProductVersion", "10.0.3")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Node", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("HttpEndpointUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<string>("TokenId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("character varying(10)");
|
||||
|
||||
b.Property<DateTimeOffset>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Nodes", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AllowUserDockerImageChange")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<int?>("DefaultDockerImageId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DonateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("UpdateUrl")
|
||||
.HasMaxLength(2048)
|
||||
.HasColumnType("character varying(2048)");
|
||||
|
||||
b.Property<string>("Version")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "FilesConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "ConfigurationFiles", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Parser")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("Path")
|
||||
.IsRequired();
|
||||
|
||||
b2.ComplexCollection(typeof(List<Dictionary<string, object>>), "Mappings", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.FilesConfig#FilesConfig.ConfigurationFiles#ConfigurationFile.Mappings#ConfigurationFileMapping", b3 =>
|
||||
{
|
||||
b3.IsRequired();
|
||||
|
||||
b3.Property<string>("Key")
|
||||
.IsRequired();
|
||||
|
||||
b3.Property<string>("Value");
|
||||
});
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("FilesConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "InstallationConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.InstallationConfig#InstallationConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<string>("DockerImage")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Script")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("Shell")
|
||||
.IsRequired();
|
||||
|
||||
b1
|
||||
.ToJson("InstallationConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "LifecycleConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.PrimitiveCollection<string>("OnlineLogPatterns")
|
||||
.IsRequired();
|
||||
|
||||
b1.Property<string>("StopCommand")
|
||||
.IsRequired();
|
||||
|
||||
b1.ComplexCollection(typeof(List<Dictionary<string, object>>), "StartupCommands", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.LifecycleConfig#LifecycleConfig.StartupCommands#StartupCommand", b2 =>
|
||||
{
|
||||
b2.IsRequired();
|
||||
|
||||
b2.Property<string>("Command")
|
||||
.IsRequired();
|
||||
|
||||
b2.Property<string>("DisplayName")
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
b1
|
||||
.ToJson("LifecycleConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.ComplexProperty(typeof(Dictionary<string, object>), "MiscellaneousConfig", "MoonlightServers.Api.Infrastructure.Database.Entities.Template.MiscellaneousConfig#MiscellaneousConfig", b1 =>
|
||||
{
|
||||
b1.IsRequired();
|
||||
|
||||
b1.Property<bool>("UseLegacyStartup");
|
||||
|
||||
b1
|
||||
.ToJson("MiscellaneousConfig")
|
||||
.HasColumnType("jsonb");
|
||||
});
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("DefaultDockerImageId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Templates", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("ImageName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<bool>("SkipPulling")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateDockerImages", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("DefaultValue")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("character varying(30)");
|
||||
|
||||
b.Property<string>("EnvName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(60)
|
||||
.HasColumnType("character varying(60)");
|
||||
|
||||
b.Property<int>("TemplateId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TemplateId");
|
||||
|
||||
b.ToTable("TemplateVariablesVariables", "servers");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", "DefaultDockerImage")
|
||||
.WithOne()
|
||||
.HasForeignKey("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "DefaultDockerImageId")
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
b.Navigation("DefaultDockerImage");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateDockerImage", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("DockerImages")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.TemplateVariable", b =>
|
||||
{
|
||||
b.HasOne("MoonlightServers.Api.Infrastructure.Database.Entities.Template", "Template")
|
||||
.WithMany("Variables")
|
||||
.HasForeignKey("TemplateId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Template");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MoonlightServers.Api.Infrastructure.Database.Entities.Template", b =>
|
||||
{
|
||||
b.Navigation("DockerImages");
|
||||
|
||||
b.Navigation("Variables");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Hybrid;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Database.Entities;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
|
||||
public class NodeTokenSchemeHandler : AuthenticationHandler<NodeTokenSchemeOptions>
|
||||
{
|
||||
public const string SchemeName = "MoonlightServers.NodeToken";
|
||||
public const string CacheKeyFormat = $"MoonlightServers.{nameof(NodeTokenSchemeHandler)}.{{0}}";
|
||||
|
||||
private readonly DatabaseRepository<Node> DatabaseRepository;
|
||||
private readonly HybridCache Cache;
|
||||
|
||||
public NodeTokenSchemeHandler(
|
||||
IOptionsMonitor<NodeTokenSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder,
|
||||
DatabaseRepository<Node> databaseRepository,
|
||||
HybridCache cache
|
||||
) : base(options, logger, encoder)
|
||||
{
|
||||
DatabaseRepository = databaseRepository;
|
||||
Cache = cache;
|
||||
}
|
||||
|
||||
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
// Basic format validation
|
||||
|
||||
if (!Context.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeaderValues))
|
||||
return AuthenticateResult.Fail("No authorization header present");
|
||||
|
||||
if (authHeaderValues.Count != 1)
|
||||
return AuthenticateResult.Fail("No authorization value present");
|
||||
|
||||
var authHeaderValue = authHeaderValues[0];
|
||||
|
||||
if (string.IsNullOrEmpty(authHeaderValue))
|
||||
return AuthenticateResult.Fail("No authorization value present");
|
||||
|
||||
var authHeaderParts = authHeaderValue.Split(
|
||||
' ',
|
||||
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries
|
||||
);
|
||||
|
||||
// Validate parts
|
||||
if (authHeaderParts.Length < 2)
|
||||
return AuthenticateResult.Fail("Malformed authorization header");
|
||||
|
||||
var tokenId = authHeaderParts[0];
|
||||
var token = authHeaderParts[1];
|
||||
|
||||
if (tokenId.Length != 10 && token.Length != 64)
|
||||
return AuthenticateResult.Fail("Malformed authorization header");
|
||||
|
||||
// Real validation
|
||||
|
||||
var cacheKey = string.Format(CacheKeyFormat, tokenId);
|
||||
|
||||
var session = await Cache.GetOrCreateAsync<NodeTokenSession?>(cacheKey, async cancellationToken =>
|
||||
{
|
||||
return await DatabaseRepository
|
||||
.Query()
|
||||
.Where(x => x.TokenId == tokenId)
|
||||
.Select(x => new NodeTokenSession(x.Id, x.Token))
|
||||
.FirstOrDefaultAsync(cancellationToken: cancellationToken);
|
||||
},
|
||||
new HybridCacheEntryOptions()
|
||||
{
|
||||
LocalCacheExpiration = Options.LookupCacheL1Expiry,
|
||||
Expiration = Options.LookupCacheL2Expiry
|
||||
}
|
||||
);
|
||||
|
||||
if(session == null || token != session.Token)
|
||||
return AuthenticateResult.Fail("Invalid authorization header");
|
||||
|
||||
// All checks have passed, create auth ticket
|
||||
return AuthenticateResult.Success(new AuthenticationTicket(
|
||||
new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
[
|
||||
new Claim("NodeId", session.Id.ToString())
|
||||
],
|
||||
SchemeName
|
||||
)
|
||||
),
|
||||
SchemeName
|
||||
));
|
||||
}
|
||||
|
||||
private record NodeTokenSession(int Id, string Token);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
|
||||
public class NodeTokenSchemeOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public TimeSpan LookupCacheL1Expiry { get; set; } = TimeSpan.FromSeconds(30);
|
||||
public TimeSpan LookupCacheL2Expiry { get; set; } = TimeSpan.FromMinutes(3);
|
||||
}
|
||||
@@ -6,6 +6,17 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.Api</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.Api</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser"/>
|
||||
@@ -18,18 +29,11 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Configuration\"/>
|
||||
<Folder Include="Database\Entities\"/>
|
||||
<Folder Include="Database\Migrations\"/>
|
||||
<Folder Include="Helpers\"/>
|
||||
<Folder Include="Implementations\"/>
|
||||
<Folder Include="Interfaces\"/>
|
||||
<Folder Include="Mappers\"/>
|
||||
<Folder Include="Models\"/>
|
||||
<Folder Include="Services\"/>
|
||||
<Folder Include="Client\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
|
||||
<ProjectReference Include="..\MoonlightServers.Shared\MoonlightServers.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
14
MoonlightServers.Api/Remote/Nodes/PingController.cs
Normal file
14
MoonlightServers.Api/Remote/Nodes/PingController.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
|
||||
namespace MoonlightServers.Api.Remote.Nodes;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/remote/servers/nodes/ping")]
|
||||
[Authorize(AuthenticationSchemes = NodeTokenSchemeHandler.SchemeName)]
|
||||
public class PingController : Controller
|
||||
{
|
||||
[HttpGet]
|
||||
public ActionResult Get() => NoContent();
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Api;
|
||||
using MoonlightServers.Api.Admin.Nodes;
|
||||
using MoonlightServers.Api.Admin.Templates;
|
||||
using MoonlightServers.Api.Infrastructure.Configuration;
|
||||
using MoonlightServers.Api.Infrastructure.Database;
|
||||
using MoonlightServers.Api.Infrastructure.Implementations.NodeToken;
|
||||
using MoonlightServers.Shared;
|
||||
using SimplePlugin.Abstractions;
|
||||
using SerializationContext = MoonlightServers.Shared.SerializationContext;
|
||||
|
||||
namespace MoonlightServers.Api;
|
||||
|
||||
@@ -17,5 +24,32 @@ public class Startup : MoonlightPlugin
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
|
||||
builder.Services.AddScoped(typeof(DatabaseRepository<>));
|
||||
|
||||
builder.Services.AddDbContext<DataContext>();
|
||||
builder.Services.AddHostedService<DbMigrationService>();
|
||||
|
||||
builder.Services.AddScoped<TemplateTransferService>();
|
||||
builder.Services.AddScoped<PterodactylEggImportService>();
|
||||
builder.Services.AddScoped<PelicanEggImportService>();
|
||||
|
||||
builder.Services.AddSingleton<NodeService>();
|
||||
|
||||
var nodeTokenOptions = new NodeTokenOptions();
|
||||
builder.Configuration.Bind("Moonlight:Servers:NodeToken", nodeTokenOptions);
|
||||
|
||||
builder.Services
|
||||
.AddAuthentication()
|
||||
.AddScheme<NodeTokenSchemeOptions, NodeTokenSchemeHandler>(NodeTokenSchemeHandler.SchemeName, options =>
|
||||
{
|
||||
options.LookupCacheL1Expiry = nodeTokenOptions.LookupCacheL1Expiry;
|
||||
options.LookupCacheL2Expiry = nodeTokenOptions.LookupCacheL2Expiry;
|
||||
});
|
||||
|
||||
builder.Logging.AddFilter(
|
||||
"MoonlightServers.Api.Infrastructure.Implementations.NodeToken.NodeTokenSchemeHandler",
|
||||
LogLevel.Warning
|
||||
);
|
||||
}
|
||||
}
|
||||
8
MoonlightServers.Daemon/Configuration/RemoteOptions.cs
Normal file
8
MoonlightServers.Daemon/Configuration/RemoteOptions.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace MoonlightServers.Daemon.Configuration;
|
||||
|
||||
public class RemoteOptions
|
||||
{
|
||||
public string EndpointUrl { get; set; }
|
||||
public string TokenId { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
||||
36
MoonlightServers.Daemon/Helpers/NativeMethods.cs
Normal file
36
MoonlightServers.Daemon/Helpers/NativeMethods.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct StatVfsResult
|
||||
{
|
||||
public ulong bsize;
|
||||
public ulong frsize;
|
||||
public ulong blocks;
|
||||
public ulong bfree;
|
||||
public ulong bavail;
|
||||
public ulong files;
|
||||
public ulong ffree;
|
||||
public ulong favail;
|
||||
public ulong fsid;
|
||||
public ulong flag;
|
||||
public ulong namemax;
|
||||
private ulong __spare0; // } kernel reserved padding —
|
||||
private ulong __spare1; // } never read, exist only to
|
||||
private ulong __spare2; // } match the 112-byte struct
|
||||
private ulong __spare3; // } statvfs layout on x86-64
|
||||
private ulong __spare4; // } Linux so the fields above
|
||||
private ulong __spare5; // } land at the right offsets
|
||||
}
|
||||
|
||||
// SetLastError = true tells the marshaller to capture errno immediately
|
||||
// after the call, before any other code can clobber it. Retrieve it with
|
||||
// Marshal.GetLastPInvokeError() which maps to the thread-local errno value.
|
||||
[LibraryImport("libc", EntryPoint = "statvfs",
|
||||
StringMarshalling = StringMarshalling.Utf8,
|
||||
SetLastError = true)]
|
||||
internal static partial int StatVfs(string path, out StatVfsResult buf);
|
||||
}
|
||||
76
MoonlightServers.Daemon/Helpers/SystemMetrics.Cpu.cs
Normal file
76
MoonlightServers.Daemon/Helpers/SystemMetrics.Cpu.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
public record CpuSnapshot(
|
||||
string ModelName,
|
||||
double TotalUsagePercent,
|
||||
IReadOnlyList<double> CoreUsagePercents
|
||||
);
|
||||
|
||||
private record RawCpuLine(
|
||||
long User,
|
||||
long Nice,
|
||||
long System,
|
||||
long Idle,
|
||||
long Iowait,
|
||||
long Irq,
|
||||
long Softirq,
|
||||
long Steal
|
||||
);
|
||||
|
||||
private static async Task<List<RawCpuLine>> ReadRawCpuStatsAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/stat");
|
||||
var result = new List<RawCpuLine>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
// All cpu* lines appear at the top of the file; stop on the first non-cpu line.
|
||||
if (!line.StartsWith("cpu", StringComparison.Ordinal))
|
||||
break;
|
||||
|
||||
var p = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (p.Length < 8)
|
||||
continue;
|
||||
|
||||
result.Add(new RawCpuLine(
|
||||
User: long.Parse(p[1]),
|
||||
Nice: long.Parse(p[2]),
|
||||
System: long.Parse(p[3]),
|
||||
Idle: long.Parse(p[4]),
|
||||
Iowait: long.Parse(p[5]),
|
||||
Irq: long.Parse(p[6]),
|
||||
Softirq: long.Parse(p[7]),
|
||||
Steal: p.Length > 8 ? long.Parse(p[8]) : 0L
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadCpuModelNameAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/cpuinfo");
|
||||
var line = lines.FirstOrDefault(l => l.StartsWith("model name", StringComparison.OrdinalIgnoreCase));
|
||||
return line is not null ? line.Split(':')[1].Trim() : "Unknown";
|
||||
}
|
||||
|
||||
private static CpuSnapshot ComputeCpuUsage(string modelName, List<RawCpuLine> s1, List<RawCpuLine> s2)
|
||||
{
|
||||
// Index 0 = aggregate "cpu" row; indices 1+ = "cpu0", "cpu1"
|
||||
var totalUsage = s1.Count > 0 ? Usage(s1[0], s2[0]) : 0.0;
|
||||
var coreUsages = s1.Skip(1).Zip(s2.Skip(1), Usage).ToList();
|
||||
|
||||
return new CpuSnapshot(modelName, totalUsage, coreUsages);
|
||||
|
||||
static double Usage(RawCpuLine a, RawCpuLine b)
|
||||
{
|
||||
var idleDelta = (b.Idle + b.Iowait) - (a.Idle + a.Iowait);
|
||||
var totalDelta = (b.User + b.Nice + b.System + b.Idle + b.Iowait + b.Irq + b.Softirq + b.Steal)
|
||||
- (a.User + a.Nice + a.System + a.Idle + a.Iowait + a.Irq + a.Softirq + a.Steal);
|
||||
return totalDelta <= 0 ? 0.0 : Math.Round((1.0 - (double)idleDelta / totalDelta) * 100.0, 2);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
149
MoonlightServers.Daemon/Helpers/SystemMetrics.Disk.cs
Normal file
149
MoonlightServers.Daemon/Helpers/SystemMetrics.Disk.cs
Normal file
@@ -0,0 +1,149 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
public record DiskInfo(
|
||||
string MountPoint,
|
||||
string Device,
|
||||
string FileSystem,
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
double UsedPercent,
|
||||
long InodesTotal,
|
||||
long InodesUsed,
|
||||
long InodesFree,
|
||||
double InodesUsedPercent
|
||||
);
|
||||
|
||||
private static readonly HashSet<string> IgnoredFileSystems = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"overlay", "aufs", // Container image layers
|
||||
"tmpfs", "devtmpfs", "ramfs", // RAM-backed virtual filesystems
|
||||
"sysfs", "proc", "devpts", // Kernel virtual filesystems
|
||||
"cgroup", "cgroup2",
|
||||
"pstore", "securityfs", "debugfs", "tracefs",
|
||||
"mqueue", "hugetlbfs", "fusectl", "configfs",
|
||||
"binfmt_misc", "nsfs", "rpc_pipefs",
|
||||
"squashfs", // Snap package loop mounts
|
||||
};
|
||||
|
||||
private static readonly string[] IgnoredMountPrefixes =
|
||||
{
|
||||
"/sys",
|
||||
"/proc",
|
||||
"/dev",
|
||||
"/run/docker",
|
||||
"/var/lib/docker",
|
||||
"/var/lib/containers",
|
||||
"/boot", "/boot/efi"
|
||||
};
|
||||
|
||||
private static async Task<IReadOnlyList<DiskInfo>> ReadDisksAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/mounts");
|
||||
var results = new List<DiskInfo>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var parts = line.Split(' ');
|
||||
if (parts.Length < 4) continue;
|
||||
|
||||
var device = parts[0];
|
||||
var mountPoint = UnescapeOctal(parts[1]);
|
||||
var fsType = parts[2];
|
||||
|
||||
if (IgnoredFileSystems.Contains(fsType)) continue;
|
||||
if (IgnoredMountPrefixes.Any(p => mountPoint.StartsWith(p, StringComparison.Ordinal))) continue;
|
||||
if (device == "none" || device.StartsWith("//", StringComparison.Ordinal)) continue;
|
||||
|
||||
var disk = ReadSingleDisk(device, mountPoint, fsType);
|
||||
|
||||
if (disk is not null)
|
||||
results.Add(disk);
|
||||
}
|
||||
|
||||
return results
|
||||
.GroupBy(d => d.Device)
|
||||
.Select(g => g.OrderBy(d => d.MountPoint.Length).First())
|
||||
.OrderBy(d => d.MountPoint)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static DiskInfo? ReadSingleDisk(string device, string mountPoint, string fsType)
|
||||
{
|
||||
if (NativeMethods.StatVfs(mountPoint, out var st) != 0)
|
||||
{
|
||||
var errno = Marshal.GetLastPInvokeError();
|
||||
Console.WriteLine($"statvfs({mountPoint}) failed: errno {errno} ({new Win32Exception(errno).Message})");
|
||||
return null;
|
||||
}
|
||||
|
||||
var blockSize = (long)st.frsize;
|
||||
var totalBytes = (long)st.blocks * blockSize;
|
||||
var freeBytes = (long)st.bavail * blockSize;
|
||||
|
||||
if (totalBytes <= 0) return null;
|
||||
|
||||
var usedBytes = totalBytes - ((long)st.bfree * blockSize);
|
||||
var usedPct = Math.Round((double)usedBytes / totalBytes * 100.0, 2);
|
||||
|
||||
long inodesTotal, inodesUsed, inodesFree;
|
||||
double inodePct;
|
||||
|
||||
if (st.files == 0)
|
||||
{
|
||||
// Filesystem doesn't expose inode counts (FAT, exFAT, NTFS via ntfs-3g, etc.)
|
||||
inodesTotal = inodesUsed = inodesFree = -1;
|
||||
inodePct = -1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
inodesTotal = (long)st.files;
|
||||
inodesFree = (long)st.ffree;
|
||||
inodesUsed = inodesTotal - inodesFree;
|
||||
inodePct = Math.Round((double)inodesUsed / inodesTotal * 100.0, 2);
|
||||
}
|
||||
|
||||
return new DiskInfo(
|
||||
MountPoint: mountPoint,
|
||||
Device: device,
|
||||
FileSystem: fsType,
|
||||
TotalBytes: totalBytes,
|
||||
UsedBytes: usedBytes,
|
||||
FreeBytes: freeBytes,
|
||||
UsedPercent: usedPct,
|
||||
InodesTotal: inodesTotal,
|
||||
InodesUsed: inodesUsed,
|
||||
InodesFree: inodesFree,
|
||||
InodesUsedPercent: inodePct
|
||||
);
|
||||
}
|
||||
|
||||
private static string UnescapeOctal(string s)
|
||||
{
|
||||
if (!s.Contains('\\')) return s;
|
||||
|
||||
var sb = new System.Text.StringBuilder(s.Length);
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (s[i] == '\\' && i + 3 < s.Length
|
||||
&& s[i + 1] is >= '0' and <= '7'
|
||||
&& s[i + 2] is >= '0' and <= '7'
|
||||
&& s[i + 3] is >= '0' and <= '7')
|
||||
{
|
||||
sb.Append((char)((s[i + 1] - '0') * 64 + (s[i + 2] - '0') * 8 + (s[i + 3] - '0')));
|
||||
i += 3;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(s[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
59
MoonlightServers.Daemon/Helpers/SystemMetrics.Memory.cs
Normal file
59
MoonlightServers.Daemon/Helpers/SystemMetrics.Memory.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
/// <summary>Memory figures derived from <c>/proc/meminfo</c>.</summary>
|
||||
/// <param name="TotalBytes">Physical RAM installed.</param>
|
||||
/// <param name="UsedBytes">RAM actively in use (<c>Total - Available</c>).</param>
|
||||
/// <param name="FreeBytes">Completely unallocated RAM.</param>
|
||||
/// <param name="CachedBytes">Page cache + reclaimable slab — matches the <c>cached</c> column in <c>free -h</c>.</param>
|
||||
/// <param name="BuffersBytes">Kernel I/O buffer memory.</param>
|
||||
/// <param name="AvailableBytes">Estimated RAM available for new allocations without swapping.</param>
|
||||
/// <param name="UsedPercent">UsedBytes / TotalBytes as a percentage (0–100).</param>
|
||||
public record MemoryInfo(
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
long CachedBytes,
|
||||
long BuffersBytes,
|
||||
long AvailableBytes,
|
||||
double UsedPercent
|
||||
);
|
||||
|
||||
// Memory — /proc/meminfo
|
||||
|
||||
/// <summary>
|
||||
/// Parses <c>/proc/meminfo</c> into a <see cref="MemoryInfo"/> record.
|
||||
/// The <c>CachedBytes</c> field is computed as
|
||||
/// <c>Cached + SReclaimable - Shmem</c> to match the value shown by <c>free -h</c>.
|
||||
/// </summary>
|
||||
private static async Task<MemoryInfo> ReadMemoryAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/meminfo");
|
||||
var map = new Dictionary<string, long>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var colon = line.IndexOf(':');
|
||||
if (colon < 0)
|
||||
continue;
|
||||
|
||||
var key = line[..colon].Trim();
|
||||
var val = line[(colon + 1)..].Trim().Split(' ')[0]; // Strip "kB" suffix.
|
||||
if (long.TryParse(val, out var kb))
|
||||
map[key] = kb * 1024L;
|
||||
}
|
||||
|
||||
var total = map.GetValueOrDefault("MemTotal");
|
||||
var available = map.GetValueOrDefault("MemAvailable");
|
||||
var free = map.GetValueOrDefault("MemFree");
|
||||
var buffers = map.GetValueOrDefault("Buffers");
|
||||
var cached = map.GetValueOrDefault("Cached")
|
||||
+ map.GetValueOrDefault("SReclaimable")
|
||||
- map.GetValueOrDefault("Shmem");
|
||||
var used = total - available;
|
||||
var usedPct = total > 0 ? Math.Round((double)used / total * 100.0, 2) : 0.0;
|
||||
|
||||
return new MemoryInfo(total, used, free, cached, buffers, available, usedPct);
|
||||
}
|
||||
}
|
||||
101
MoonlightServers.Daemon/Helpers/SystemMetrics.Network.cs
Normal file
101
MoonlightServers.Daemon/Helpers/SystemMetrics.Network.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
public partial class SystemMetrics
|
||||
{
|
||||
/// <summary>Network throughput for a single interface, computed between two samples.</summary>
|
||||
/// <param name="Name">Interface name, e.g. <c>eth0</c>, <c>ens3</c>.</param>
|
||||
/// <param name="RxBytesPerSec">Received bytes per second.</param>
|
||||
/// <param name="TxBytesPerSec">Transmitted bytes per second.</param>
|
||||
/// <param name="RxPacketsPerSec">Received packets per second.</param>
|
||||
/// <param name="TxPacketsPerSec">Transmitted packets per second.</param>
|
||||
/// <param name="RxErrors">Cumulative receive error count (not a rate).</param>
|
||||
/// <param name="TxErrors">Cumulative transmit error count (not a rate).</param>
|
||||
public record NetworkInterfaceInfo(
|
||||
string Name,
|
||||
long RxBytesPerSec,
|
||||
long TxBytesPerSec,
|
||||
long RxPacketsPerSec,
|
||||
long TxPacketsPerSec,
|
||||
long RxErrors,
|
||||
long TxErrors
|
||||
);
|
||||
|
||||
// Network
|
||||
private record RawNetLine(
|
||||
string Iface,
|
||||
long RxBytes,
|
||||
long RxPackets,
|
||||
long RxErrors,
|
||||
long TxBytes,
|
||||
long TxPackets,
|
||||
long TxErrors
|
||||
);
|
||||
|
||||
private static async Task<List<RawNetLine>> ReadRawNetStatsAsync()
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync("/proc/net/dev");
|
||||
var result = new List<RawNetLine>();
|
||||
|
||||
// The first two lines are the column-header banner.
|
||||
foreach (var line in lines.Skip(2))
|
||||
{
|
||||
var colon = line.IndexOf(':');
|
||||
if (colon < 0)
|
||||
continue;
|
||||
|
||||
var iface = line[..colon].Trim();
|
||||
|
||||
// Skip loopback and ephemeral veth pairs created by container runtimes.
|
||||
if (iface == "lo" || iface.StartsWith("veth", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
var f = line[(colon + 1)..].Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (f.Length < 16)
|
||||
continue;
|
||||
|
||||
// Column layout (after the colon, 0-based):
|
||||
// RX: [0]bytes [1]packets [2]errs [3]drop [4]fifo [5]frame [6]compressed [7]multicast
|
||||
// TX: [8]bytes [9]packets [10]errs [11]drop [12]fifo [13]colls [14]carrier [15]compressed
|
||||
result.Add(new RawNetLine(
|
||||
Iface: iface,
|
||||
RxBytes: long.Parse(f[0]),
|
||||
RxPackets: long.Parse(f[1]),
|
||||
RxErrors: long.Parse(f[2]),
|
||||
TxBytes: long.Parse(f[8]),
|
||||
TxPackets: long.Parse(f[9]),
|
||||
TxErrors: long.Parse(f[10])
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<NetworkInterfaceInfo> ComputeNetworkRates(
|
||||
List<RawNetLine> s1,
|
||||
List<RawNetLine> s2,
|
||||
double intervalSecs
|
||||
)
|
||||
{
|
||||
var prev = s1.ToDictionary(x => x.Iface);
|
||||
var result = new List<NetworkInterfaceInfo>();
|
||||
var div = intervalSecs > 0 ? intervalSecs : 1.0;
|
||||
|
||||
foreach (var cur in s2)
|
||||
{
|
||||
if (!prev.TryGetValue(cur.Iface, out var p))
|
||||
continue;
|
||||
|
||||
result.Add(new NetworkInterfaceInfo(
|
||||
Name: cur.Iface,
|
||||
RxBytesPerSec: (long)((cur.RxBytes - p.RxBytes) / div),
|
||||
TxBytesPerSec: (long)((cur.TxBytes - p.TxBytes) / div),
|
||||
RxPacketsPerSec: (long)((cur.RxPackets - p.RxPackets) / div),
|
||||
TxPacketsPerSec: (long)((cur.TxPackets - p.TxPackets) / div),
|
||||
RxErrors: cur.RxErrors,
|
||||
TxErrors: cur.TxErrors
|
||||
));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
59
MoonlightServers.Daemon/Helpers/SystemMetrics.cs
Normal file
59
MoonlightServers.Daemon/Helpers/SystemMetrics.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
namespace MoonlightServers.Daemon.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Reads Linux system metrics directly from the <c>/proc</c> and <c>/sys</c>
|
||||
/// pseudo-filesystems
|
||||
/// </summary>
|
||||
public static partial class SystemMetrics
|
||||
{
|
||||
public record SystemSnapshot(
|
||||
CpuSnapshot Cpu,
|
||||
MemoryInfo Memory,
|
||||
IReadOnlyList<DiskInfo> Disks,
|
||||
IReadOnlyList<NetworkInterfaceInfo> Network,
|
||||
TimeSpan Uptime
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Collects a full system snapshot. The method waits <paramref name="sampleIntervalMs"/>
|
||||
/// milliseconds between the two samples required to compute CPU and network rates.
|
||||
/// All other reads happen in parallel during the second sampling window.
|
||||
/// </summary>
|
||||
/// <param name="sampleIntervalMs">
|
||||
/// Interval between rate-measurement samples in milliseconds.
|
||||
/// Larger values yield smoother CPU and network averages. Defaults to <c>500</c>.
|
||||
/// </param>
|
||||
/// <returns>A fully populated <see cref="SystemSnapshot"/>.</returns>
|
||||
public static async Task<SystemSnapshot> ReadAllAsync(int sampleIntervalMs = 500)
|
||||
{
|
||||
// First samples — must complete before the delay.
|
||||
var cpuSample1Task = ReadRawCpuStatsAsync();
|
||||
var netSample1Task = ReadRawNetStatsAsync();
|
||||
await Task.WhenAll(cpuSample1Task, netSample1Task);
|
||||
|
||||
await Task.Delay(sampleIntervalMs);
|
||||
|
||||
// Second samples + all independent reads run concurrently.
|
||||
var cpuSample2Task = ReadRawCpuStatsAsync();
|
||||
var netSample2Task = ReadRawNetStatsAsync();
|
||||
var memTask = ReadMemoryAsync();
|
||||
var diskTask = ReadDisksAsync();
|
||||
var uptimeTask = ReadUptimeAsync();
|
||||
var cpuNameTask = ReadCpuModelNameAsync();
|
||||
|
||||
await Task.WhenAll(cpuSample2Task, netSample2Task, memTask, diskTask, uptimeTask, cpuNameTask);
|
||||
|
||||
var cpu = ComputeCpuUsage(cpuNameTask.Result, cpuSample1Task.Result, cpuSample2Task.Result);
|
||||
var network = ComputeNetworkRates(netSample1Task.Result, netSample2Task.Result, sampleIntervalMs / 1000.0);
|
||||
|
||||
return new SystemSnapshot(cpu, memTask.Result, diskTask.Result, network, uptimeTask.Result);
|
||||
}
|
||||
|
||||
// Uptime
|
||||
private static async Task<TimeSpan> ReadUptimeAsync()
|
||||
{
|
||||
var text = await File.ReadAllTextAsync("/proc/uptime");
|
||||
var seconds = double.Parse(text.Split(' ')[0], System.Globalization.CultureInfo.InvariantCulture);
|
||||
return TimeSpan.FromSeconds(seconds);
|
||||
}
|
||||
}
|
||||
30
MoonlightServers.Daemon/Http/Controllers/HealthController.cs
Normal file
30
MoonlightServers.Daemon/Http/Controllers/HealthController.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
namespace MoonlightServers.Daemon.Http.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/health")]
|
||||
public class HealthController : Controller
|
||||
{
|
||||
private readonly RemoteService RemoteService;
|
||||
|
||||
public HealthController(RemoteService remoteService)
|
||||
{
|
||||
RemoteService = remoteService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<HealthDto>> GetAsync()
|
||||
{
|
||||
var remoteStatusCode = await RemoteService.CheckReachabilityAsync();
|
||||
|
||||
return new HealthDto()
|
||||
{
|
||||
RemoteStatusCode = remoteStatusCode
|
||||
};
|
||||
}
|
||||
}
|
||||
22
MoonlightServers.Daemon/Http/Controllers/SystemController.cs
Normal file
22
MoonlightServers.Daemon/Http/Controllers/SystemController.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.Daemon.Mappers;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
namespace MoonlightServers.Daemon.Http.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[ApiController]
|
||||
[Route("api/system")]
|
||||
public class SystemController : Controller
|
||||
{
|
||||
[HttpGet("statistics")]
|
||||
public async Task<ActionResult<SystemStatisticsDto>> GetStatisticsAsync()
|
||||
{
|
||||
var snapshot = await SystemMetrics.ReadAllAsync();
|
||||
var statistics = SystemStatisticsMapper.ToDto(snapshot);
|
||||
|
||||
return statistics;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
namespace MoonlightServers.Daemon.Implementations.TokenScheme;
|
||||
|
||||
public class TokenSchemeHandler : AuthenticationHandler<TokenSchemeOptions>
|
||||
{
|
||||
public const string SchemeName = "MoonlightServers.Token";
|
||||
|
||||
public TokenSchemeHandler(
|
||||
IOptionsMonitor<TokenSchemeOptions> options,
|
||||
ILoggerFactory logger,
|
||||
UrlEncoder encoder
|
||||
) : base(options, logger, encoder)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
if (!Context.Request.Headers.TryGetValue(HeaderNames.Authorization, out var authHeaderValues))
|
||||
return Task.FromResult(AuthenticateResult.Fail("No authorization header present"));
|
||||
|
||||
if (authHeaderValues.Count != 1)
|
||||
return Task.FromResult(AuthenticateResult.Fail("No authorization value present"));
|
||||
|
||||
var authHeaderValue = authHeaderValues[0];
|
||||
|
||||
if (string.IsNullOrEmpty(authHeaderValue))
|
||||
return Task.FromResult(AuthenticateResult.Fail("No authorization value present"));
|
||||
|
||||
if (authHeaderValue != Options.Token)
|
||||
return Task.FromResult(AuthenticateResult.Fail("Invalid token provided"));
|
||||
|
||||
return Task.FromResult(
|
||||
AuthenticateResult.Success(new AuthenticationTicket(
|
||||
new ClaimsPrincipal(new ClaimsIdentity([], nameof(TokenSchemeHandler))),
|
||||
nameof(TokenSchemeHandler)
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
namespace MoonlightServers.Daemon.Implementations.TokenScheme;
|
||||
|
||||
public class TokenSchemeOptions : AuthenticationSchemeOptions
|
||||
{
|
||||
public string Token { get; set; }
|
||||
}
|
||||
14
MoonlightServers.Daemon/Mappers/SystemStatisticsMapper.cs
Normal file
14
MoonlightServers.Daemon/Mappers/SystemStatisticsMapper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Daemon.Mappers;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class SystemStatisticsMapper
|
||||
{
|
||||
public static partial SystemStatisticsDto ToDto(SystemMetrics.SystemSnapshot snapshot);
|
||||
}
|
||||
@@ -4,10 +4,12 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Docker.DotNet.Enhanced" Version="3.132.0" />
|
||||
<PackageReference Include="Riok.Mapperly" Version="5.0.0-next.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -26,6 +28,22 @@
|
||||
<Compile Update="ServerSystem\Server.Update.cs">
|
||||
<DependentUpon>Server.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Cpu.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Memory.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Network.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Update="Helpers\SystemMetrics.Disk.cs">
|
||||
<DependentUpon>SystemMetrics.cs</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MoonlightServers.DaemonShared\MoonlightServers.DaemonShared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.Daemon.Helpers;
|
||||
using MoonlightServers.Daemon.Implementations.TokenScheme;
|
||||
using MoonlightServers.Daemon.ServerSystem;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Docker;
|
||||
using MoonlightServers.Daemon.ServerSystem.Implementations.Local;
|
||||
using MoonlightServers.Daemon.Services;
|
||||
using MoonlightServers.DaemonShared.Http;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -18,7 +21,28 @@ builder.Services.AddSingleton<ServerService>();
|
||||
builder.Services.AddDockerServices();
|
||||
builder.Services.AddLocalServices();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
builder.Services.AddHttpClient();
|
||||
builder.Services.AddSingleton<RemoteService>();
|
||||
|
||||
builder.Services.AddOptions<RemoteOptions>().BindConfiguration("Moonlight:Remote");
|
||||
|
||||
var remoteOptions = new RemoteOptions();
|
||||
builder.Configuration.Bind("Moonlight:Remote", remoteOptions);
|
||||
|
||||
builder.Services.AddControllers()
|
||||
.AddApplicationPart(typeof(Program).Assembly)
|
||||
.AddJsonOptions(options =>
|
||||
{
|
||||
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication(TokenSchemeHandler.SchemeName)
|
||||
.AddScheme<TokenSchemeOptions, TokenSchemeHandler>(TokenSchemeHandler.SchemeName, options =>
|
||||
{
|
||||
options.Token = remoteOptions!.Token;
|
||||
});
|
||||
|
||||
builder.Logging.AddFilter("MoonlightServers.Daemon.Implementations.TokenScheme.TokenSchemeHandler", LogLevel.Warning);
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -50,8 +74,6 @@ Task.Run(async () =>
|
||||
await server.StopAsync();
|
||||
|
||||
Console.ReadLine();
|
||||
|
||||
await serverService.DeleteAsync("a0e3ddb4-2c72-4f4c-bc49-35650a4bc5c0");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
117
MoonlightServers.Daemon/Services/RemoteService.cs
Normal file
117
MoonlightServers.Daemon/Services/RemoteService.cs
Normal file
@@ -0,0 +1,117 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using MoonlightServers.Daemon.Configuration;
|
||||
using MoonlightServers.DaemonShared.Http;
|
||||
|
||||
namespace MoonlightServers.Daemon.Services;
|
||||
|
||||
public class RemoteService
|
||||
{
|
||||
private readonly IOptions<RemoteOptions> Options;
|
||||
private readonly ILogger<RemoteService> Logger;
|
||||
private readonly IHttpClientFactory ClientFactory;
|
||||
|
||||
public RemoteService(
|
||||
IOptions<RemoteOptions> options,
|
||||
IHttpClientFactory clientFactory,
|
||||
ILogger<RemoteService> logger
|
||||
)
|
||||
{
|
||||
Options = options;
|
||||
ClientFactory = clientFactory;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task<int> CheckReachabilityAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = ClientFactory.CreateClient();
|
||||
|
||||
var request = CreateBaseRequest(HttpMethod.Get, "api/remote/servers/nodes/ping");
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
return (int)response.StatusCode;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogTrace(e, "An error occured while checking if remote is reachable");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateBaseRequest(
|
||||
[StringSyntax(StringSyntaxAttribute.Uri)]
|
||||
HttpMethod method,
|
||||
string endpoint
|
||||
)
|
||||
{
|
||||
var request = new HttpRequestMessage();
|
||||
|
||||
request.Headers.Add(HeaderNames.Authorization, $"{Options.Value.TokenId} {Options.Value.Token}");
|
||||
request.RequestUri = new Uri(new Uri(Options.Value.EndpointUrl), endpoint);
|
||||
request.Method = method;
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
private async Task EnsureSuccessAsync(HttpResponseMessage message)
|
||||
{
|
||||
if (message.IsSuccessStatusCode)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var problemDetails = await message.Content.ReadFromJsonAsync<ProblemDetails>(
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (problemDetails == null)
|
||||
{
|
||||
// If we cant handle problem details, we handle it natively
|
||||
message.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse into exception
|
||||
throw new RemoteException(
|
||||
problemDetails.Type,
|
||||
problemDetails.Title,
|
||||
problemDetails.Status,
|
||||
problemDetails.Detail,
|
||||
problemDetails.Errors
|
||||
);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// If we cant handle problem details, we handle it natively
|
||||
message.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class RemoteException : Exception
|
||||
{
|
||||
public string Type { get; }
|
||||
public string Title { get; }
|
||||
public int Status { get; }
|
||||
public string? Detail { get; }
|
||||
public Dictionary<string, string[]>? Errors { get; }
|
||||
|
||||
public RemoteException(
|
||||
string type,
|
||||
string title,
|
||||
int status,
|
||||
string? detail = null,
|
||||
Dictionary<string, string[]>? errors = null)
|
||||
: base(detail ?? title)
|
||||
{
|
||||
Type = type;
|
||||
Title = title;
|
||||
Status = status;
|
||||
Detail = detail;
|
||||
Errors = errors;
|
||||
}
|
||||
}
|
||||
6
MoonlightServers.DaemonShared/Http/Daemon/HealthDto.cs
Normal file
6
MoonlightServers.DaemonShared/Http/Daemon/HealthDto.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
public class HealthDto
|
||||
{
|
||||
public int RemoteStatusCode { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
namespace MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
public record SystemStatisticsDto(
|
||||
CpuSnapshotDto Cpu,
|
||||
MemoryInfoDto Memory,
|
||||
IReadOnlyList<DiskInfoDto> Disks,
|
||||
IReadOnlyList<NetworkInterfaceInfoDto> Network,
|
||||
TimeSpan Uptime
|
||||
);
|
||||
|
||||
public record CpuSnapshotDto(
|
||||
string ModelName,
|
||||
double TotalUsagePercent,
|
||||
IReadOnlyList<double> CoreUsagePercents
|
||||
);
|
||||
|
||||
public record MemoryInfoDto(
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
long CachedBytes,
|
||||
long BuffersBytes,
|
||||
long AvailableBytes,
|
||||
double UsedPercent
|
||||
);
|
||||
|
||||
public record DiskInfoDto(
|
||||
string MountPoint,
|
||||
string Device,
|
||||
string FileSystem,
|
||||
long TotalBytes,
|
||||
long UsedBytes,
|
||||
long FreeBytes,
|
||||
double UsedPercent,
|
||||
long InodesTotal,
|
||||
long InodesUsed,
|
||||
long InodesFree,
|
||||
double InodesUsedPercent
|
||||
);
|
||||
|
||||
public record NetworkInterfaceInfoDto(
|
||||
string Name,
|
||||
long RxBytesPerSec,
|
||||
long TxBytesPerSec,
|
||||
long RxPacketsPerSec,
|
||||
long TxPacketsPerSec,
|
||||
long RxErrors,
|
||||
long TxErrors
|
||||
);
|
||||
14
MoonlightServers.DaemonShared/Http/ProblemDetails.cs
Normal file
14
MoonlightServers.DaemonShared/Http/ProblemDetails.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace MoonlightServers.DaemonShared.Http;
|
||||
|
||||
public class ProblemDetails
|
||||
{
|
||||
public string Type { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public int Status { get; set; }
|
||||
|
||||
public string? Detail { get; set; }
|
||||
|
||||
public Dictionary<string, string[]>? Errors { get; set; }
|
||||
}
|
||||
15
MoonlightServers.DaemonShared/Http/SerializationContext.cs
Normal file
15
MoonlightServers.DaemonShared/Http/SerializationContext.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using MoonlightServers.DaemonShared.Http.Daemon;
|
||||
|
||||
namespace MoonlightServers.DaemonShared.Http;
|
||||
|
||||
[JsonSerializable(typeof(HealthDto))]
|
||||
[JsonSerializable(typeof(SystemStatisticsDto))]
|
||||
[JsonSerializable(typeof(ProblemDetails))]
|
||||
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
|
||||
public partial class SerializationContext : JsonSerializerContext
|
||||
{
|
||||
|
||||
}
|
||||
@@ -6,4 +6,20 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.DaemonShared</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.DaemonShared</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Http\Panel\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
75
MoonlightServers.Frontend/Admin/Index.razor
Normal file
75
MoonlightServers.Frontend/Admin/Index.razor
Normal file
@@ -0,0 +1,75 @@
|
||||
@page "/admin/servers"
|
||||
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Shared
|
||||
@using ShadcnBlazor.Tab
|
||||
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
@inject NavigationManager Navigation
|
||||
|
||||
@attribute [Authorize(Policy = Permissions.Servers.View)]
|
||||
|
||||
<Tabs DefaultValue="@(Tab ?? "servers")" OnValueChanged="OnTabChanged">
|
||||
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
|
||||
<TabsTrigger Value="servers" Disabled="@(!ServersAccess.Succeeded)">
|
||||
<ContainerIcon />
|
||||
Servers
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="nodes" Disabled="@(!NodesAccess.Succeeded)">
|
||||
<ServerIcon />
|
||||
Nodes
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="templates" Disabled="@(!TemplatesAccess.Succeeded)">
|
||||
<Package2Icon />
|
||||
Templates
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="manager" Disabled="@(!NodesAccess.Succeeded)">
|
||||
<TableIcon />
|
||||
Manager
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@if (ServersAccess.Succeeded)
|
||||
{
|
||||
<TabsContent Value="servers">
|
||||
|
||||
</TabsContent>
|
||||
}
|
||||
|
||||
@if (NodesAccess.Succeeded)
|
||||
{
|
||||
<TabsContent Value="nodes">
|
||||
<MoonlightServers.Frontend.Admin.Nodes.Overview />
|
||||
</TabsContent>
|
||||
}
|
||||
|
||||
@if (TemplatesAccess.Succeeded)
|
||||
{
|
||||
<TabsContent Value="templates">
|
||||
<MoonlightServers.Frontend.Admin.Templates.Overview />
|
||||
</TabsContent>
|
||||
}
|
||||
</Tabs>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter]
|
||||
[SupplyParameterFromQuery(Name = "tab")]
|
||||
public string? Tab { get; set; }
|
||||
|
||||
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||
|
||||
private AuthorizationResult ServersAccess;
|
||||
private AuthorizationResult NodesAccess;
|
||||
private AuthorizationResult TemplatesAccess;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthState;
|
||||
|
||||
ServersAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Servers.View);
|
||||
NodesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.View);
|
||||
TemplatesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.View);
|
||||
}
|
||||
|
||||
private void OnTabChanged(string tab) => Navigation.NavigateTo($"/admin/servers?tab={tab}");
|
||||
}
|
||||
104
MoonlightServers.Frontend/Admin/Nodes/Create.razor
Normal file
104
MoonlightServers.Frontend/Admin/Nodes/Create.razor
Normal file
@@ -0,0 +1,104 @@
|
||||
@page "/admin/servers/nodes/create"
|
||||
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ToastService ToastService
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Create Node</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Create a new node
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<SubmitButton>
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="nodeName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="nodeHttpEndpoint">HTTP Endpoint</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.HttpEndpointUrl"
|
||||
id="nodeHttpEndpoint"
|
||||
placeholder="http://example.com:8080"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
private CreateNodeDto Request = new();
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/servers/nodes",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Node Creation",
|
||||
$"Successfully created node {Request.Name}"
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/admin/servers?tab=nodes");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
110
MoonlightServers.Frontend/Admin/Nodes/NodeHealthDisplay.razor
Normal file
110
MoonlightServers.Frontend/Admin/Nodes/NodeHealthDisplay.razor
Normal file
@@ -0,0 +1,110 @@
|
||||
@using Microsoft.Extensions.Logging
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Tooltips
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ILogger<NodeHealthDisplay> Logger
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<span class="text-muted-foreground">Loading</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (IsHealthy)
|
||||
{
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Slot>
|
||||
<span class="text-green-400" @attributes="context">Healthy</span>
|
||||
</Slot>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@TooltipText
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Slot>
|
||||
<span class="text-destructive" @attributes="context">Unhealthy</span>
|
||||
</Slot>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@TooltipText
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
}
|
||||
}
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public NodeDto Node { get; set; }
|
||||
|
||||
private bool IsLoading = true;
|
||||
|
||||
private string TooltipText = "An unknown error has occured. Check logs";
|
||||
private bool IsHealthy;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await HttpClient.GetFromJsonAsync<NodeHealthDto>($"api/admin/servers/nodes/{Node.Id}/health", SerializationContext.Default.Options);
|
||||
|
||||
if(result == null)
|
||||
return;
|
||||
|
||||
IsHealthy = result.IsHealthy;
|
||||
|
||||
if (IsHealthy)
|
||||
TooltipText = "Version: v2.1.0"; // TODO: Add version loading
|
||||
else
|
||||
{
|
||||
if (result.StatusCode != 0)
|
||||
{
|
||||
if (result.StatusCode is >= 200 and <= 299)
|
||||
{
|
||||
if (result.RemoteStatusCode != 0)
|
||||
{
|
||||
TooltipText = result.RemoteStatusCode switch
|
||||
{
|
||||
401 => "Daemon is unable to authenticate against the panel",
|
||||
404 => "Daemon is unable to request the panel's endpoint",
|
||||
500 => "Panel encountered an internal server error",
|
||||
_ => $"Panel returned {result.RemoteStatusCode}"
|
||||
};
|
||||
}
|
||||
else
|
||||
TooltipText = "Daemon is unable to reach the panel";
|
||||
}
|
||||
else
|
||||
{
|
||||
TooltipText = result.StatusCode switch
|
||||
{
|
||||
401 => "Panel is unable to authenticate against the node",
|
||||
404 => "Panel is unable to request the daemon's endpoint",
|
||||
500 => "Daemon encountered an internal server error",
|
||||
_ => $"Daemon returned {result.StatusCode}"
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
TooltipText = "Moonlight is unable to reach the node";
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An unhandled error occured while fetching the node health status");
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
152
MoonlightServers.Frontend/Admin/Nodes/Overview.razor
Normal file
152
MoonlightServers.Frontend/Admin/Nodes/Overview.razor
Normal file
@@ -0,0 +1,152 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Tabels
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject AlertDialogService AlertDialogService
|
||||
@inject ToastService ToastService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
|
||||
<div class="flex flex-row justify-between mt-5">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Nodes</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Manage nodes
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button>
|
||||
<Slot Context="buttonCtx">
|
||||
<a @attributes="buttonCtx" href="/admin/servers/nodes/create"
|
||||
data-disabled="@(!CreateAccess.Succeeded)">
|
||||
<PlusIcon/>
|
||||
Create
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<DataGrid @ref="Grid" TGridItem="NodeDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
|
||||
<PropertyColumn Field="u => u.Id"/>
|
||||
<TemplateColumn IsFilterable="true" Identifier="@nameof(NodeDto.Name)" Title="Name">
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<a class="text-primary" href="#"
|
||||
@onclick="() => Edit(context)" @onclick:preventDefault>
|
||||
@context.Name
|
||||
</a>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn IsFilterable="false" Title="Status">
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<NodeHealthDisplay Node="context" />
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Title="HTTP Endpoint"
|
||||
Field="u => u.HttpEndpointUrl"/>
|
||||
<TemplateColumn>
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<div class="flex flex-row items-center justify-end me-3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Slot Context="dropdownSlot">
|
||||
<Button Size="ButtonSize.IconSm" Variant="ButtonVariant.Ghost"
|
||||
@attributes="dropdownSlot">
|
||||
<EllipsisIcon/>
|
||||
</Button>
|
||||
</Slot>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent SideOffset="2">
|
||||
<DropdownMenuItem OnClick="() => Edit(context)"
|
||||
Disabled="@(!EditAccess.Succeeded)">
|
||||
Edit
|
||||
<DropdownMenuShortcut>
|
||||
<PenIcon/>
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem OnClick="() => DeleteAsync(context)"
|
||||
Variant="DropdownMenuItemVariant.Destructive"
|
||||
Disabled="@(!DeleteAccess.Succeeded)">
|
||||
Delete
|
||||
<DropdownMenuShortcut>
|
||||
<TrashIcon/>
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</DataGrid>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||
|
||||
private DataGrid<NodeDto> Grid;
|
||||
|
||||
private AuthorizationResult EditAccess;
|
||||
private AuthorizationResult DeleteAccess;
|
||||
private AuthorizationResult CreateAccess;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthState;
|
||||
|
||||
EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.Edit);
|
||||
DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.Delete);
|
||||
CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Nodes.Create);
|
||||
}
|
||||
|
||||
private async Task<DataGridResponse<NodeDto>> LoadAsync(DataGridRequest<NodeDto> request)
|
||||
{
|
||||
var query = $"?startIndex={request.StartIndex}&length={request.Length}";
|
||||
var filterOptions = request.Filters.Count > 0 ? new FilterOptions(request.Filters) : null;
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<NodeDto>>(
|
||||
$"api/admin/servers/nodes{query}&filterOptions={filterOptions}",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
return new DataGridResponse<NodeDto>(response!.Data, response.TotalLength);
|
||||
}
|
||||
|
||||
private void Edit(NodeDto context) => NavigationManager.NavigateTo($"/admin/servers/nodes/{context.Id}");
|
||||
|
||||
private async Task DeleteAsync(NodeDto context)
|
||||
{
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
"Node Deletion",
|
||||
$"Do you really want to delete the node {context.Name}? This cannot be undone.",
|
||||
async () =>
|
||||
{
|
||||
var response = await HttpClient.DeleteAsync($"api/admin/servers/nodes/{context.Id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Node Deletion",
|
||||
$"Successfully deleted node {context.Name}"
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
85
MoonlightServers.Frontend/Admin/Nodes/SettingsTab.razor
Normal file
85
MoonlightServers.Frontend/Admin/Nodes/SettingsTab.razor
Normal file
@@ -0,0 +1,85 @@
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="nodeName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="nodeName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="nodeHttpEndpoint">HTTP Endpoint</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.HttpEndpointUrl"
|
||||
id="nodeHttpEndpoint"
|
||||
placeholder="http://example.com:8080"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter ClassName="justify-end">
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter, EditorRequired] public NodeDto Node { get; set; }
|
||||
|
||||
private UpdateNodeDto Request;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new UpdateNodeDto()
|
||||
{
|
||||
Name = Node.Name,
|
||||
HttpEndpointUrl = Node.HttpEndpointUrl
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PutAsJsonAsync(
|
||||
$"/api/admin/servers/nodes/{Node.Id}",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Node Update",
|
||||
$"Successfully updated node {Request.Name}"
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
301
MoonlightServers.Frontend/Admin/Nodes/StatisticsTab.razor
Normal file
301
MoonlightServers.Frontend/Admin/Nodes/StatisticsTab.razor
Normal file
@@ -0,0 +1,301 @@
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Frontend.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Spinners
|
||||
@using ShadcnBlazor.Progresses
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<h3 class="text-base font-semibold mt-5 mb-2">Overview</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="CpuChart" T="double"
|
||||
Title="CPU Usage"
|
||||
DisplayField="@(d => $"{Math.Round(d, 2)}%")"
|
||||
ValueField="d => d"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="MemoryChart" T="MemoryDataPoint"
|
||||
Title="Memory Usage"
|
||||
DisplayField="@(d => Formatter.FormatSize(d.UsedMemory))"
|
||||
ValueField="d => d.Percent"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="NetworkInChart" T="long"
|
||||
Title="Incoming Traffic"
|
||||
DisplayField="@(d => $"{Formatter.FormatSize(d)}/s")"
|
||||
Min="0"
|
||||
Max="512"
|
||||
ValueField="@(d => d / 1024f / 1024f)"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1">
|
||||
<RealtimeChart @ref="NetworkOutChart" T="long"
|
||||
Title="Outgoing Traffic"
|
||||
DisplayField="@(d => $"{Formatter.FormatSize(d)}/s")"
|
||||
Min="0"
|
||||
Max="512"
|
||||
ValueField="@(d => d / 1024f / 1024f)"
|
||||
ClassName="h-32"/>
|
||||
</div>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (HasLoaded && StatisticsDto != null)
|
||||
{
|
||||
<CardHeader ClassName="gap-0">
|
||||
<CardDescription>Uptime</CardDescription>
|
||||
<CardTitle ClassName="text-lg">@Formatter.FormatDuration(StatisticsDto.Uptime)</CardTitle>
|
||||
</CardHeader>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
@if (HasLoaded && HealthDto != null)
|
||||
{
|
||||
<CardHeader ClassName="gap-0">
|
||||
<CardDescription>Health Status</CardDescription>
|
||||
<CardTitle ClassName="text-lg">
|
||||
@if (HealthDto.IsHealthy)
|
||||
{
|
||||
<span class="text-green-500">Healthy</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-destructive">Unhealthy</span>
|
||||
}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
}
|
||||
else
|
||||
{
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
<Spinner ClassName="size-8"/>
|
||||
</CardContent>
|
||||
}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<h3 class="text-base font-semibold mt-8 mb-2">Details</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>System Details</CardTitle>
|
||||
<CardDescription>Details over your general system configuration</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@if (StatisticsDto == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<CpuIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No details</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No details about your system found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="space-y-3">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground font-medium">CPU Model</span>
|
||||
<span>
|
||||
@StatisticsDto.Cpu.ModelName
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground font-medium">Total Memory</span>
|
||||
<span>
|
||||
@Formatter.FormatSize(StatisticsDto.Memory.TotalBytes)
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-muted-foreground font-medium">Total Disk Space</span>
|
||||
<span>
|
||||
@{
|
||||
var totalDiskSpace = StatisticsDto
|
||||
.Disks
|
||||
.DistinctBy(x => x.Device)
|
||||
.Sum(x => x.TotalBytes);
|
||||
}
|
||||
|
||||
@Formatter.FormatSize(totalDiskSpace)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card ClassName="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Disk Details</CardTitle>
|
||||
<CardDescription>Details over all your mounted disks</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@if (StatisticsDto == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<HardDriveIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No details</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
No details about disk and their usage found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="space-y-4">
|
||||
@foreach (var disk in StatisticsDto.Disks)
|
||||
{
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-medium">
|
||||
@disk.MountPoint
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
@disk.FileSystem
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
@disk.Device
|
||||
</span>
|
||||
</div>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
@Formatter.FormatSize(disk.UsedBytes) / @Formatter.FormatSize(disk.TotalBytes)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Progress Value="(int)disk.UsedPercent" Max="100"></Progress>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public NodeDto Node { get; set; }
|
||||
|
||||
private NodeHealthDto? HealthDto;
|
||||
private NodeStatisticsDto? StatisticsDto;
|
||||
|
||||
private bool HasLoaded;
|
||||
|
||||
private RealtimeChart<double>? CpuChart;
|
||||
|
||||
private RealtimeChart<long>? NetworkInChart;
|
||||
|
||||
private RealtimeChart<long>? NetworkOutChart;
|
||||
|
||||
private RealtimeChart<MemoryDataPoint>? MemoryChart;
|
||||
private Timer? Timer;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
HealthDto = await HttpClient.GetFromJsonAsync<NodeHealthDto>(
|
||||
$"api/admin/servers/nodes/{Node.Id}/health",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (HealthDto is { IsHealthy: true })
|
||||
{
|
||||
StatisticsDto = await HttpClient.GetFromJsonAsync<NodeStatisticsDto>(
|
||||
$"api/admin/servers/nodes/{Node.Id}/statistics",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
}
|
||||
|
||||
HasLoaded = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
Timer = new Timer(RefreshCallbackAsync, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
||||
}
|
||||
|
||||
private async void RefreshCallbackAsync(object? state)
|
||||
{
|
||||
try
|
||||
{
|
||||
StatisticsDto = await HttpClient.GetFromJsonAsync<NodeStatisticsDto>(
|
||||
$"api/admin/servers/nodes/{Node.Id}/statistics",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (StatisticsDto == null) return;
|
||||
|
||||
if (CpuChart != null)
|
||||
await CpuChart.PushAsync(StatisticsDto.Cpu.TotalUsagePercent);
|
||||
|
||||
if (MemoryChart != null)
|
||||
await MemoryChart.PushAsync(new MemoryDataPoint(StatisticsDto.Memory.UsedBytes, StatisticsDto.Memory.UsedPercent));
|
||||
|
||||
if (NetworkInChart != null && NetworkOutChart != null)
|
||||
{
|
||||
var networkInterface = StatisticsDto
|
||||
.Network
|
||||
.FirstOrDefault(x => x.Name.StartsWith("eth"));
|
||||
|
||||
if (networkInterface == null)
|
||||
{
|
||||
networkInterface = StatisticsDto
|
||||
.Network
|
||||
.FirstOrDefault(x => x.Name.StartsWith("en"));
|
||||
}
|
||||
|
||||
if (networkInterface == null)
|
||||
return;
|
||||
|
||||
await NetworkInChart.PushAsync(networkInterface.RxBytesPerSec);
|
||||
await NetworkOutChart.PushAsync(networkInterface.TxBytesPerSec);
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (Timer != null)
|
||||
await Timer.DisposeAsync();
|
||||
|
||||
if (CpuChart != null)
|
||||
await CpuChart.DisposeAsync();
|
||||
}
|
||||
|
||||
private record MemoryDataPoint(long UsedMemory, double Percent);
|
||||
}
|
||||
90
MoonlightServers.Frontend/Admin/Nodes/View.razor
Normal file
90
MoonlightServers.Frontend/Admin/Nodes/View.razor
Normal file
@@ -0,0 +1,90 @@
|
||||
@page "/admin/servers/nodes/{Id:int}"
|
||||
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Nodes
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Tab
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
@attribute [Authorize(Policy = Permissions.Nodes.View)]
|
||||
|
||||
<LazyLoader Load="LoadAsync">
|
||||
@if (Dto == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<SearchIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Node not found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
A node with this id cannot be found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">@Dto.Name</h1>
|
||||
<div class="text-muted-foreground">
|
||||
View details for @Dto.Name
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers?tab=nodes" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<Tabs DefaultValue="statistics">
|
||||
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
|
||||
<TabsTrigger Value="statistics">
|
||||
<ChartColumnBigIcon/>
|
||||
Statistics
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="settings">
|
||||
<SettingsIcon/>
|
||||
Settings
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent Value="statistics">
|
||||
<StatisticsTab Node="Dto" />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="settings">
|
||||
<SettingsTab Node="Dto" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
</div>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
private NodeDto? Dto;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
Dto = await HttpClient.GetFromJsonAsync<NodeDto>(
|
||||
$"api/admin/servers/nodes/{Id}",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Tabels
|
||||
|
||||
@inherits Editor<List<MoonlightServers.Shared.Admin.Templates.UpdateConfigurationFileMappingDto>>
|
||||
|
||||
<div class="rounded-md bg-card shadow-sm border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
Key
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
Value
|
||||
</TableHead>
|
||||
<TableHead ClassName="w-10"/>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@foreach (var mapping in Value)
|
||||
{
|
||||
<TableRow @key="mapping">
|
||||
<TableCell>
|
||||
<TextInputField @bind-Value="mapping.Key"
|
||||
placeholder="server-port"/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInputField @bind-Value="mapping.Value"
|
||||
placeholder="{{SERVER_PORT}}"/>
|
||||
</TableCell>
|
||||
<TableCell ClassName="text-right pr-4">
|
||||
<Button @onclick="() => DeleteAsync(mapping)"
|
||||
Size="ButtonSize.Icon"
|
||||
Variant="ButtonVariant.Destructive">
|
||||
<Trash2Icon/>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow>
|
||||
<TableCell colspan="999999">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
@onclick="AddAsync"
|
||||
Variant="ButtonVariant.Outline"
|
||||
Size="ButtonSize.Sm">
|
||||
<PlusIcon/>
|
||||
Add Mapping
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private async Task DeleteAsync(UpdateConfigurationFileMappingDto mappingDto)
|
||||
{
|
||||
Value.Remove(mappingDto);
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private async Task AddAsync()
|
||||
{
|
||||
Value.Add(new UpdateConfigurationFileMappingDto()
|
||||
{
|
||||
Key = "Change me",
|
||||
Value = "{{CHANGE_ME}}"
|
||||
});
|
||||
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Accordions
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
<div class="mb-5 flex justify-end">
|
||||
<Button @onclick="Add">
|
||||
<PlusIcon/>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||
@foreach (var file in FilesConfig.ConfigurationFiles)
|
||||
{
|
||||
<Accordion ClassName="-space-y-px" Type="AccordionType.Single">
|
||||
<AccordionItem
|
||||
ClassName="overflow-hidden border bg-card px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
|
||||
Value="element">
|
||||
<AccordionTrigger ClassName="hover:no-underline">
|
||||
<div class="flex items-center gap-3">
|
||||
<FileCogIcon/>
|
||||
<span class="text-left">@file.Path</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent ClassName="ps-7">
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<Field>
|
||||
@{
|
||||
var id = $"configFilePath{file.GetHashCode()}";
|
||||
}
|
||||
<FieldLabel for="@id">Path</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="file.Path"
|
||||
id="@id"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
@{
|
||||
var id = $"configFileParser{file.GetHashCode()}";
|
||||
}
|
||||
<FieldLabel for="@id">Parser</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="file.Parser"
|
||||
id="@id"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<FieldSet>
|
||||
<ConfigFileMappingEditor @bind-Value="file.Mappings" />
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<Button Variant="ButtonVariant.Destructive"
|
||||
@onclick="() => Delete(file)">
|
||||
Delete
|
||||
</Button>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public UpdateFilesConfigDto FilesConfig { get; set; }
|
||||
|
||||
private void Add()
|
||||
=> FilesConfig.ConfigurationFiles.Add(new());
|
||||
|
||||
private void Delete(UpdateConfigurationFileDto dto)
|
||||
=> FilesConfig.ConfigurationFiles.Remove(dto);
|
||||
}
|
||||
131
MoonlightServers.Frontend/Admin/Templates/Create.razor
Normal file
131
MoonlightServers.Frontend/Admin/Templates/Create.razor
Normal file
@@ -0,0 +1,131 @@
|
||||
@page "/admin/servers/templates/create"
|
||||
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ToastService ToastService
|
||||
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Create Template</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Create a new template
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers?tab=templates" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<SubmitButton>
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<div class="mt-8">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="templateName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="templateName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateAuthor">Author</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Author"
|
||||
id="templateAuthor"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateVersion">Version</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Version"
|
||||
id="templateVersion"/>
|
||||
</Field>
|
||||
|
||||
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
|
||||
<FieldLabel for="templateDescription">Description</FieldLabel>
|
||||
<TextareaInputField
|
||||
@bind-Value="Request.Description"
|
||||
id="templateDescription"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateDonateUrl">Donate URL</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.DonateUrl"
|
||||
id="templateDonateUrl"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateUpdateUrl">Update URL</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.UpdateUrl"
|
||||
id="templateUpdateUrl"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</EnhancedEditForm>
|
||||
|
||||
@code
|
||||
{
|
||||
private CreateTemplateDto Request = new();
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
"/api/admin/servers/templates",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Template Creation",
|
||||
$"Successfully created template {Request.Name}"
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/admin/servers?tab=templates");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Switches
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Docker Image</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new docker image
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<EnhancedEditForm @ref="Form" Model="Model" OnValidSubmit="OnValidSubmitAsync">
|
||||
<FieldGroup>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="dockerImageDisplayName">Display Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Model.DisplayName"
|
||||
id="dockerImageDisplayName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="dockerImageIdentifier">Image Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Model.ImageName"
|
||||
id="dockerImageIdentifier"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="dockerImageSkipPulling">Skip Pulling</FieldLabel>
|
||||
<Switch id="dockerImageSkipPulling" @bind-Value="Model.SkipPulling" />
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
<Slot>
|
||||
<Button Type="button" Variant="ButtonVariant.Outline" @attributes="context">
|
||||
Cancel
|
||||
</Button>
|
||||
</Slot>
|
||||
</DialogClose>
|
||||
<Button @onclick="() => Form.SubmitAsync()" Type="button">
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateDockerImageDto Model = new();
|
||||
private EnhancedEditForm Form;
|
||||
|
||||
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
$"api/admin/servers/templates/{Template.Id}/dockerImages",
|
||||
Model,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Model, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Docker Image Creation",
|
||||
$"Successfully created variable {Model.DisplayName}"
|
||||
);
|
||||
|
||||
await CloseAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Dialogs
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Variable</DialogTitle>
|
||||
<DialogDescription>
|
||||
Create a new variable
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<EnhancedEditForm @ref="Form" Model="Model" OnValidSubmit="OnValidSubmitAsync">
|
||||
<FieldGroup>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
<FieldLabel for="variableDisplayName">Display Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Model.DisplayName"
|
||||
id="variableDisplayName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="variableDescription">Description</FieldLabel>
|
||||
<TextareaInputField
|
||||
@bind-Value="Model.Description"
|
||||
id="variableDescription"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="variableKey">Key</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Model.EnvName"
|
||||
id="variableKey"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="variableDefaultValue">Default Value</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Model.DefaultValue"
|
||||
id="variableDefaultValue"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
<Slot>
|
||||
<Button Type="button" Variant="ButtonVariant.Outline" @attributes="context">
|
||||
Cancel
|
||||
</Button>
|
||||
</Slot>
|
||||
</DialogClose>
|
||||
<Button @onclick="() => Form.SubmitAsync()" Type="button">
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||
[Parameter] public Func<Task> OnSubmit { get; set; }
|
||||
|
||||
private CreateVariableDto Model = new();
|
||||
private EnhancedEditForm Form;
|
||||
|
||||
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PostAsJsonAsync(
|
||||
$"api/admin/servers/templates/{Template.Id}/variables",
|
||||
Model,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Model, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await OnSubmit.Invoke();
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Variable Creation",
|
||||
$"Successfully created variable {Model.DisplayName}"
|
||||
);
|
||||
|
||||
await CloseAsync();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
@using System.Text.Json
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Selects
|
||||
@using ShadcnBlazor.Switches
|
||||
@using ShadcnBlazor.Tabels
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
@inject AlertDialogService AlertDialogService
|
||||
@inject DialogService DialogService
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="LoadAsync">
|
||||
<Card ClassName="mb-5">
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="templateAllowUserDockerImageChange">Allow User Docker Image Change
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
@bind-Value="Request.AllowUserDockerImageChange"
|
||||
id="templateAllowUserDockerImageChange"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>Default Docker Image</FieldLabel>
|
||||
<Select @bind-Value="DefaultDockerImageBinder">
|
||||
<SelectTrigger>
|
||||
<SelectValue Placeholder="Select a docker image" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@foreach (var (id, dockerImage) in DockerImages)
|
||||
{
|
||||
<SelectItem Value="@id.ToString()">
|
||||
@dockerImage.DisplayName
|
||||
</SelectItem>
|
||||
}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div class="overflow-hidden rounded-md border bg-card shadow-sm">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
Display Name
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
Identifier
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
Skip Pulling
|
||||
</TableHead>
|
||||
<TableHead ClassName="w-10"/>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@foreach (var (id, dockerImage) in DockerImages)
|
||||
{
|
||||
<TableRow
|
||||
@key="dockerImage">
|
||||
<TableCell>
|
||||
<TextInputField
|
||||
@bind-Value="dockerImage.DisplayName"
|
||||
placeholder="Default Image"/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInputField
|
||||
@bind-Value="dockerImage.ImageName"
|
||||
placeholder="debian:latest"/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Switch @bind-Value="dockerImage.SkipPulling"/>
|
||||
</TableCell>
|
||||
<TableCell ClassName="text-right pr-4">
|
||||
<Button @onclick="() => UpdateAsync(id, dockerImage)"
|
||||
Size="ButtonSize.Icon"
|
||||
Variant="ButtonVariant.Outline">
|
||||
<SaveIcon/>
|
||||
</Button>
|
||||
<Button @onclick="() => DeleteAsync(id, dockerImage)"
|
||||
Size="ButtonSize.Icon"
|
||||
Variant="ButtonVariant.Destructive">
|
||||
<Trash2Icon/>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow>
|
||||
<TableCell colspan="999999">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
@onclick="AddAsync"
|
||||
Variant="ButtonVariant.Outline"
|
||||
Size="ButtonSize.Sm">
|
||||
<PlusIcon/>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||
[Parameter] public UpdateTemplateDto Request { get; set; }
|
||||
|
||||
private readonly List<(int, UpdateDockerImageDto)> DockerImages = new();
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private string? DefaultDockerImageBinder
|
||||
{
|
||||
get => Request.DefaultDockerImageId?.ToString();
|
||||
set
|
||||
{
|
||||
if (int.TryParse(value, out var intValue))
|
||||
Request.DefaultDockerImageId = intValue;
|
||||
else
|
||||
Request.DefaultDockerImageId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
DockerImages.Clear();
|
||||
|
||||
var totalAmount = 0;
|
||||
var currentIndex = 0;
|
||||
const int pageSize = 50;
|
||||
|
||||
do
|
||||
{
|
||||
var dockerImages = await HttpClient.GetFromJsonAsync<PagedData<DockerImageDto>>(
|
||||
$"api/admin/servers/templates/{Template.Id}/dockerImages?startIndex={currentIndex}&length={pageSize}"
|
||||
);
|
||||
|
||||
if (dockerImages == null)
|
||||
continue;
|
||||
|
||||
currentIndex += dockerImages.Data.Length;
|
||||
totalAmount = dockerImages.TotalLength;
|
||||
|
||||
DockerImages.AddRange(dockerImages.Data.Select(x => (x.Id, new UpdateDockerImageDto()
|
||||
{
|
||||
DisplayName = x.DisplayName,
|
||||
ImageName = x.ImageName,
|
||||
SkipPulling = x.SkipPulling
|
||||
})));
|
||||
} while (DockerImages.Count < totalAmount);
|
||||
}
|
||||
|
||||
private async Task DeleteAsync(int id, UpdateDockerImageDto dto)
|
||||
{
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
"Docker Image Deletion",
|
||||
$"Do you really want to delete the docker image {dto.DisplayName}? This cannot be undone",
|
||||
async () =>
|
||||
{
|
||||
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{Template.Id}/dockerImages/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Docker Image Deletion",
|
||||
$"Successfully deleted docker image {dto.DisplayName}"
|
||||
);
|
||||
|
||||
await LazyLoader.ReloadAsync();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task UpdateAsync(int id, UpdateDockerImageDto dto)
|
||||
{
|
||||
var response = await HttpClient.PutAsJsonAsync(
|
||||
$"api/admin/servers/templates/{Template.Id}/dockerImages/{id}",
|
||||
dto,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
try
|
||||
{
|
||||
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>(
|
||||
Moonlight.Shared.SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (problemDetails == null)
|
||||
{
|
||||
// Fallback
|
||||
response.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (problemDetails.Errors is { Count: > 0 })
|
||||
{
|
||||
var errorMessages = string.Join(
|
||||
", ",
|
||||
problemDetails.Errors.Select(x => $"{x.Key}: {x.Value.FirstOrDefault()}")
|
||||
);
|
||||
|
||||
await ToastService.ErrorAsync(
|
||||
"Docker Image Update",
|
||||
$"{problemDetails.Detail ?? problemDetails.Title} {errorMessages}"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
await ToastService.ErrorAsync(
|
||||
"Docker Image Update",
|
||||
$"{problemDetails.Detail ?? problemDetails.Title}"
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
// Fallback if unable to deserialize
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Docker Image Update",
|
||||
$"Successfully updated docker image {dto.DisplayName}"
|
||||
);
|
||||
|
||||
await LazyLoader.ReloadAsync();
|
||||
}
|
||||
|
||||
private async Task AddAsync()
|
||||
{
|
||||
await DialogService.LaunchAsync<CreateDockerImageDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(CreateDockerImageDialog.Template)] = Template;
|
||||
parameters[nameof(CreateDockerImageDialog.OnSubmit)] = async () => { await LazyLoader.ReloadAsync(); };
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@using LucideBlazor
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Tabels
|
||||
@using ShadcnBlazor.Buttons
|
||||
|
||||
@inherits Editor<List<string>>
|
||||
|
||||
<div class="rounded-md bg-card shadow-sm border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
Online Text
|
||||
</TableHead>
|
||||
<TableHead ClassName="w-10"/>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@foreach (var command in Value)
|
||||
{
|
||||
<TableRow
|
||||
@key="command">
|
||||
<TableCell>
|
||||
<TextInputField Value="@command"
|
||||
ValueExpression="() => command" disabled/>
|
||||
</TableCell>
|
||||
<TableCell ClassName="text-right pr-4">
|
||||
<Button @onclick="() => DeleteAsync(command)"
|
||||
Size="ButtonSize.Icon"
|
||||
Variant="ButtonVariant.Destructive">
|
||||
<Trash2Icon/>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow>
|
||||
<TableCell colspan="999999">
|
||||
<div class="flex justify-end gap-1">
|
||||
<TextInputField ClassName="h-8"
|
||||
@bind-Value="Input"
|
||||
placeholder="Enter text..." />
|
||||
<Button
|
||||
@onclick="AddAsync"
|
||||
Variant="ButtonVariant.Outline"
|
||||
Size="ButtonSize.Sm">
|
||||
<PlusIcon/>
|
||||
Add Online Text
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private string Input;
|
||||
|
||||
private async Task DeleteAsync(string command)
|
||||
{
|
||||
Value.Remove(command);
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private async Task AddAsync()
|
||||
{
|
||||
if(string.IsNullOrEmpty(Input))
|
||||
return;
|
||||
|
||||
if(Value.Contains(Input))
|
||||
return;
|
||||
|
||||
Value.Add(Input);
|
||||
Input = "";
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
}
|
||||
201
MoonlightServers.Frontend/Admin/Templates/Overview.razor
Normal file
201
MoonlightServers.Frontend/Admin/Templates/Overview.razor
Normal file
@@ -0,0 +1,201 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.DataGrids
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Dropdowns
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Tabels
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject AlertDialogService AlertDialogService
|
||||
@inject ToastService ToastService
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IAuthorizationService AuthorizationService
|
||||
|
||||
<InputFile OnChange="OnFileSelectedAsync" id="import-template" class="hidden" multiple accept=".yml,.yaml,.json"/>
|
||||
|
||||
<div class="flex flex-row justify-between mt-5">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Templates</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Manage templates
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Outline">
|
||||
<Slot>
|
||||
<label for="import-template" @attributes="context">
|
||||
<HardDriveUploadIcon/>
|
||||
Import
|
||||
</label>
|
||||
</Slot>
|
||||
</Button>
|
||||
<Button>
|
||||
<Slot Context="buttonCtx">
|
||||
<a @attributes="buttonCtx" href="/admin/servers/templates/create"
|
||||
data-disabled="@(!CreateAccess.Succeeded)">
|
||||
<PlusIcon/>
|
||||
Create
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<DataGrid @ref="Grid" TGridItem="TemplateDto" Loader="LoadAsync" PageSize="10" ClassName="bg-card">
|
||||
<PropertyColumn Field="u => u.Id"/>
|
||||
<TemplateColumn IsFilterable="true" Identifier="@nameof(TemplateDto.Name)" Title="@nameof(TemplateDto.Name)">
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<a class="text-primary" href="#"
|
||||
@onclick="() => Edit(context)" @onclick:preventDefault>
|
||||
@context.Name
|
||||
</a>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<TemplateColumn Title="@nameof(TemplateDto.Description)" HeadClassName="hidden lg:table-cell">
|
||||
<CellTemplate>
|
||||
<TableCell ClassName="hidden lg:table-cell">
|
||||
<div class="truncate max-w-md">@context.Description</div>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
<PropertyColumn Field="u => u.Author"
|
||||
HeadClassName="hidden xl:table-cell"
|
||||
CellClassName="hidden xl:table-cell"/>
|
||||
<PropertyColumn Field="u => u.Version"
|
||||
HeadClassName="hidden xl:table-cell"
|
||||
CellClassName="hidden xl:table-cell"/>
|
||||
<TemplateColumn>
|
||||
<CellTemplate>
|
||||
<TableCell>
|
||||
<div class="flex flex-row items-center justify-end me-3">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Slot Context="dropdownSlot">
|
||||
<Button Size="ButtonSize.IconSm" Variant="ButtonVariant.Ghost"
|
||||
@attributes="dropdownSlot">
|
||||
<EllipsisIcon/>
|
||||
</Button>
|
||||
</Slot>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent SideOffset="2">
|
||||
<DropdownMenuItem OnClick="() => Export(context)">
|
||||
Export
|
||||
<DropdownMenuShortcut>
|
||||
<HardDriveDownloadIcon/>
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem OnClick="() => Edit(context)"
|
||||
Disabled="@(!EditAccess.Succeeded)">
|
||||
Edit
|
||||
<DropdownMenuShortcut>
|
||||
<PenIcon/>
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem OnClick="() => DeleteAsync(context)"
|
||||
Variant="DropdownMenuItemVariant.Destructive"
|
||||
Disabled="@(!DeleteAccess.Succeeded)">
|
||||
Delete
|
||||
<DropdownMenuShortcut>
|
||||
<TrashIcon/>
|
||||
</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</TableCell>
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</DataGrid>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
|
||||
|
||||
private DataGrid<TemplateDto> Grid;
|
||||
|
||||
private AuthorizationResult EditAccess;
|
||||
private AuthorizationResult DeleteAccess;
|
||||
private AuthorizationResult CreateAccess;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
var authState = await AuthState;
|
||||
|
||||
EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Edit);
|
||||
DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Delete);
|
||||
CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Templates.Create);
|
||||
}
|
||||
|
||||
private async Task<DataGridResponse<TemplateDto>> LoadAsync(DataGridRequest<TemplateDto> request)
|
||||
{
|
||||
var query = $"?startIndex={request.StartIndex}&length={request.Length}";
|
||||
var filterOptions = request.Filters.Count > 0 ? new FilterOptions(request.Filters) : null;
|
||||
|
||||
var response = await HttpClient.GetFromJsonAsync<PagedData<TemplateDto>>(
|
||||
$"api/admin/servers/templates{query}&filterOptions={filterOptions}",
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
return new DataGridResponse<TemplateDto>(response!.Data, response.TotalLength);
|
||||
}
|
||||
|
||||
private void Edit(TemplateDto context) => NavigationManager.NavigateTo($"/admin/servers/templates/{context.Id}");
|
||||
|
||||
private void Export(TemplateDto dto) => NavigationManager.NavigateTo($"api/admin/servers/templates/{dto.Id}/export", true);
|
||||
|
||||
private async Task DeleteAsync(TemplateDto context)
|
||||
{
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
"Template Deletion",
|
||||
$"Do you really want to delete the template {context.Name}? This cannot be undone.",
|
||||
async () =>
|
||||
{
|
||||
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{context.Id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Template Deletion",
|
||||
$"Successfully deleted template {context.Name}"
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async Task OnFileSelectedAsync(InputFileChangeEventArgs eventArgs)
|
||||
{
|
||||
var files = eventArgs.GetMultipleFiles();
|
||||
|
||||
foreach (var browserFile in files)
|
||||
{
|
||||
await using var contentStream = browserFile.OpenReadStream(browserFile.Size);
|
||||
|
||||
var response = await HttpClient.PostAsync(
|
||||
"api/admin/servers/templates/import",
|
||||
new StreamContent(contentStream)
|
||||
);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var importedTemplate = await response
|
||||
.Content
|
||||
.ReadFromJsonAsync<TemplateDto>(SerializationContext.Default.Options);
|
||||
|
||||
if (importedTemplate == null)
|
||||
continue;
|
||||
|
||||
await Grid.RefreshAsync();
|
||||
await ToastService.SuccessAsync("Template Import", $"Successfully imported template {importedTemplate.Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
16
MoonlightServers.Frontend/Admin/Templates/ScriptEditor.razor
Normal file
16
MoonlightServers.Frontend/Admin/Templates/ScriptEditor.razor
Normal file
@@ -0,0 +1,16 @@
|
||||
@using ShadcnBlazor.Extras.Editors
|
||||
|
||||
@inherits Editor<string>
|
||||
|
||||
<Editor @ref="Editor" Language="EditorLanguage.None" InitialValue="@Value" />
|
||||
|
||||
@code
|
||||
{
|
||||
private Editor Editor;
|
||||
|
||||
public async Task SubmitAsync()
|
||||
{
|
||||
Value = await Editor.GetValueAsync();
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
@using LucideBlazor
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Tabels
|
||||
|
||||
@inherits Editor<List<MoonlightServers.Shared.Admin.Templates.UpdateStartupCommandDto>>
|
||||
|
||||
<div class="rounded-md bg-card shadow-sm border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>
|
||||
Display Name
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
Command
|
||||
</TableHead>
|
||||
<TableHead ClassName="w-10"/>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@foreach (var command in Value)
|
||||
{
|
||||
<TableRow
|
||||
@key="command">
|
||||
<TableCell>
|
||||
<TextInputField @bind-Value="command.DisplayName"
|
||||
placeholder="Default Command"/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInputField @bind-Value="command.Command"
|
||||
placeholder="java -Xmx{{SERVER_MEMORY}} server.jar"/>
|
||||
</TableCell>
|
||||
<TableCell ClassName="text-right pr-4">
|
||||
<Button @onclick="() => DeleteAsync(command)"
|
||||
Size="ButtonSize.Icon"
|
||||
Variant="ButtonVariant.Destructive">
|
||||
<Trash2Icon/>
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
}
|
||||
<TableRow>
|
||||
<TableCell colspan="999999">
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
@onclick="AddAsync"
|
||||
Variant="ButtonVariant.Outline"
|
||||
Size="ButtonSize.Sm">
|
||||
<PlusIcon/>
|
||||
Add Startup Command
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private async Task DeleteAsync(UpdateStartupCommandDto commandDto)
|
||||
{
|
||||
Value.Remove(commandDto);
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private async Task AddAsync()
|
||||
{
|
||||
Value.Add(new UpdateStartupCommandDto()
|
||||
{
|
||||
Command = "Change me",
|
||||
DisplayName = "Change me"
|
||||
});
|
||||
await ValueChanged.InvokeAsync(Value);
|
||||
}
|
||||
}
|
||||
13
MoonlightServers.Frontend/Admin/Templates/TemplateMapper.cs
Normal file
13
MoonlightServers.Frontend/Admin/Templates/TemplateMapper.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using MoonlightServers.Shared.Admin.Templates;
|
||||
using Riok.Mapperly.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Frontend.Admin.Templates;
|
||||
|
||||
[Mapper]
|
||||
[SuppressMessage("Mapper", "RMG020:No members are mapped in an object mapping")]
|
||||
[SuppressMessage("Mapper", "RMG012:No members are mapped in an object mapping")]
|
||||
public static partial class TemplateMapper
|
||||
{
|
||||
public static partial UpdateTemplateDto ToRequest(DetailedTemplateDto dto);
|
||||
}
|
||||
296
MoonlightServers.Frontend/Admin/Templates/Update.razor
Normal file
296
MoonlightServers.Frontend/Admin/Templates/Update.razor
Normal file
@@ -0,0 +1,296 @@
|
||||
@page "/admin/servers/templates/{Id:int}"
|
||||
|
||||
@using System.Net
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
@using ShadcnBlazor.Tab
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject NavigationManager Navigation
|
||||
@inject ToastService ToastService
|
||||
|
||||
<LazyLoader Load="LoadAsync">
|
||||
@if (Template == null)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<SearchIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>Template not found</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
A template with this id cannot be found
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="formContext">
|
||||
|
||||
<div class="flex flex-row justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h1 class="text-xl font-semibold">Update Template</h1>
|
||||
<div class="text-muted-foreground">
|
||||
Update template @Template.Name
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row gap-x-1.5">
|
||||
<Button Variant="ButtonVariant.Secondary">
|
||||
<Slot>
|
||||
<a href="/admin/servers?tab=templates" @attributes="context">
|
||||
<ChevronLeftIcon/>
|
||||
Back
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
<SubmitButton>
|
||||
<CheckIcon/>
|
||||
Continue
|
||||
</SubmitButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 space-y-5">
|
||||
|
||||
<DataAnnotationsValidator/>
|
||||
|
||||
<FormValidationSummary/>
|
||||
|
||||
<Tabs DefaultValue="meta">
|
||||
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
|
||||
<TabsTrigger Value="meta">
|
||||
<IdCardIcon/>
|
||||
Meta
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="lifecycle">
|
||||
<PlayIcon/>
|
||||
Lifecycle
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="installation">
|
||||
<SquareTerminalIcon/>
|
||||
Installation
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="variables">
|
||||
<VariableIcon/>
|
||||
Variables
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="dockerImages">
|
||||
<ContainerIcon/>
|
||||
Docker Images
|
||||
</TabsTrigger>
|
||||
<TabsTrigger Value="files">
|
||||
<FileCogIcon/>
|
||||
Files
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent Value="meta">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="templateName">Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Name"
|
||||
id="templateName"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateAuthor">Author</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Author"
|
||||
id="templateAuthor"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateVersion">Version</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.Version"
|
||||
id="templateVersion"/>
|
||||
</Field>
|
||||
|
||||
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
|
||||
<FieldLabel for="templateDescription">Description</FieldLabel>
|
||||
<TextareaInputField
|
||||
@bind-Value="Request.Description"
|
||||
id="templateDescription"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateDonateUrl">Donate URL</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.DonateUrl"
|
||||
id="templateDonateUrl"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateUpdateUrl">Update URL</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.UpdateUrl"
|
||||
id="templateUpdateUrl"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="lifecycle">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field ClassName="col-span-1 lg:col-span-2">
|
||||
<FieldLabel>Startup Commands</FieldLabel>
|
||||
<StartupCommandEditor
|
||||
@bind-Value="Request.LifecycleConfig.StartupCommands"/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="templateStopCommand">Stop Command</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.LifecycleConfig.StopCommand"
|
||||
id="templateStopCommand"/>
|
||||
</Field>
|
||||
<Field ClassName="col-span-1 lg:col-span-2">
|
||||
<FieldLabel>Online Texts</FieldLabel>
|
||||
<OnlineTextEditor
|
||||
@bind-Value="Request.LifecycleConfig.OnlineLogPatterns"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="installation">
|
||||
<Card>
|
||||
<CardContent>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
<FieldGroup>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
<Field>
|
||||
<FieldLabel for="templateInstallDockerImage">Docker Image
|
||||
</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.InstallationConfig.DockerImage"
|
||||
id="templateInstallDockerImage"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel for="templateInstallShell">Shell</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.InstallationConfig.Shell"
|
||||
id="templateInstallShell"/>
|
||||
</Field>
|
||||
|
||||
<Field ClassName="col-span-1 lg:col-span-2 xl:col-span-3">
|
||||
<FieldLabel>Script</FieldLabel>
|
||||
<ScriptEditor
|
||||
@ref="ScriptEditor"
|
||||
@bind-Value="Request.InstallationConfig.Script"/>
|
||||
</Field>
|
||||
</div>
|
||||
</FieldGroup>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="variables">
|
||||
<VariableListEditor Template="Template"/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="dockerImages">
|
||||
<DockerImageListEditor Request="Request" Template="Template"/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent Value="files">
|
||||
<ConfigFilesEditor FilesConfig="Request.FilesConfig"/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
</EnhancedEditForm>
|
||||
}
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public int Id { get; set; }
|
||||
|
||||
private UpdateTemplateDto Request;
|
||||
private DetailedTemplateDto? Template;
|
||||
|
||||
private ScriptEditor ScriptEditor;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
// Meta
|
||||
var metaResponse = await HttpClient.GetAsync($"api/admin/servers/templates/{Id}");
|
||||
|
||||
if (!metaResponse.IsSuccessStatusCode)
|
||||
{
|
||||
if (metaResponse.StatusCode == HttpStatusCode.NotFound)
|
||||
return;
|
||||
|
||||
metaResponse.EnsureSuccessStatusCode();
|
||||
return;
|
||||
}
|
||||
|
||||
Template = await metaResponse.Content.ReadFromJsonAsync<DetailedTemplateDto>(
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (Template == null)
|
||||
return;
|
||||
|
||||
Request = TemplateMapper.ToRequest(Template);
|
||||
}
|
||||
|
||||
private async Task<bool> OnSubmitAsync(EditContext context, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
await ScriptEditor.SubmitAsync();
|
||||
|
||||
var response = await HttpClient.PutAsJsonAsync(
|
||||
$"/api/admin/servers/templates/{Id}",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Template Update",
|
||||
$"Successfully updated template {Request.Name}"
|
||||
);
|
||||
|
||||
Navigation.NavigateTo("/admin/servers?tab=templates");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
147
MoonlightServers.Frontend/Admin/Templates/VariableEditor.razor
Normal file
147
MoonlightServers.Frontend/Admin/Templates/VariableEditor.razor
Normal file
@@ -0,0 +1,147 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Frontend.Infrastructure.Helpers
|
||||
@using MoonlightServers.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Accordions
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Extras.AlertDialogs
|
||||
@using ShadcnBlazor.Extras.Forms
|
||||
@using ShadcnBlazor.Extras.Toasts
|
||||
@using ShadcnBlazor.Fields
|
||||
@using ShadcnBlazor.Inputs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject ToastService ToastService
|
||||
@inject AlertDialogService AlertDialogService
|
||||
|
||||
<Accordion ClassName="-space-y-px" Type="AccordionType.Single">
|
||||
<AccordionItem
|
||||
ClassName="overflow-hidden border bg-card px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
|
||||
Value="element">
|
||||
<AccordionTrigger ClassName="hover:no-underline">
|
||||
<div class="flex items-center gap-3">
|
||||
<VariableIcon/>
|
||||
<span class="text-left">@Request.DisplayName</span>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent ClassName="ps-7">
|
||||
<EnhancedEditForm Model="Request" OnValidSubmit="OnValidSubmitAsync">
|
||||
<FieldGroup>
|
||||
<DataAnnotationsValidator/>
|
||||
<FormValidationSummary/>
|
||||
|
||||
<FieldSet>
|
||||
<Field>
|
||||
@{
|
||||
var id = $"variableDisplayName{Variable.Id}";
|
||||
}
|
||||
<FieldLabel for="@id">Display Name</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.DisplayName"
|
||||
id="@id"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
@{
|
||||
var id = $"variableDescription{Variable.Id}";
|
||||
}
|
||||
<FieldLabel for="@id">Description</FieldLabel>
|
||||
<TextareaInputField
|
||||
@bind-Value="Request.Description"
|
||||
id="@id"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
@{
|
||||
var id = $"variableKey{Variable.Id}";
|
||||
}
|
||||
<FieldLabel for="@id">Key</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.EnvName"
|
||||
id="@id"/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
@{
|
||||
var id = $"variableDefaultValue{Variable.Id}";
|
||||
}
|
||||
<FieldLabel for="@id">Default Value</FieldLabel>
|
||||
<TextInputField
|
||||
@bind-Value="Request.DefaultValue"
|
||||
id="@id"/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
|
||||
<Button Variant="ButtonVariant.Destructive" @onclick="DeleteAsync">Delete</Button>
|
||||
<SubmitButton>Save changes</SubmitButton>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
</EnhancedEditForm>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public VariableDto Variable { get; set; }
|
||||
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||
[Parameter] public Func<Task> OnChanged { get; set; }
|
||||
|
||||
private UpdateVariableDto Request;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Request = new UpdateVariableDto()
|
||||
{
|
||||
DisplayName = Variable.DisplayName,
|
||||
DefaultValue = Variable.DefaultValue,
|
||||
EnvName = Variable.EnvName,
|
||||
Description = Variable.Description
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> OnValidSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
|
||||
{
|
||||
var response = await HttpClient.PutAsJsonAsync(
|
||||
$"api/admin/servers/templates/{Template.Id}/variables/{Variable.Id}",
|
||||
Request,
|
||||
SerializationContext.Default.Options
|
||||
);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
|
||||
return false;
|
||||
}
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Variable Update",
|
||||
$"Successfully updated variable {Request.DisplayName}"
|
||||
);
|
||||
|
||||
await OnChanged.Invoke();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task DeleteAsync()
|
||||
{
|
||||
await AlertDialogService.ConfirmDangerAsync(
|
||||
"Variable Deletion",
|
||||
$"Do you really want to delete the variable {Variable.DisplayName}? This cannot be undone",
|
||||
async () =>
|
||||
{
|
||||
var response = await HttpClient.DeleteAsync($"api/admin/servers/templates/{Template.Id}/variables/{Variable.Id}");
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await ToastService.SuccessAsync(
|
||||
"Variable Deletion",
|
||||
$"Variable {Variable.DisplayName} successfully deleted"
|
||||
);
|
||||
|
||||
await OnChanged.Invoke();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Shared
|
||||
@using MoonlightServers.Shared.Admin.Templates
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Extras.Dialogs
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
@inject DialogService DialogService
|
||||
|
||||
<div class="flex flex-row justify-end mb-5">
|
||||
<Button @onclick="LaunchCreateAsync">
|
||||
<PlusIcon />
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<LazyLoader @ref="LazyLoader" Load="LoadAsync">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8">
|
||||
@foreach (var variable in Variables)
|
||||
{
|
||||
<VariableEditor OnChanged="() => LazyLoader.ReloadAsync()"
|
||||
Template="Template"
|
||||
Variable="variable"
|
||||
@key="variable" />
|
||||
}
|
||||
</div>
|
||||
</LazyLoader>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public DetailedTemplateDto Template { get; set; }
|
||||
|
||||
private readonly List<VariableDto> Variables = new();
|
||||
private LazyLoader LazyLoader;
|
||||
|
||||
private async Task LoadAsync(LazyLoader _)
|
||||
{
|
||||
Variables.Clear();
|
||||
|
||||
var totalAmount = 0;
|
||||
var currentIndex = 0;
|
||||
const int pageSize = 50;
|
||||
|
||||
do
|
||||
{
|
||||
var variables = await HttpClient.GetFromJsonAsync<PagedData<VariableDto>>(
|
||||
$"api/admin/servers/templates/{Template.Id}/variables?startIndex={currentIndex}&length={pageSize}"
|
||||
);
|
||||
|
||||
if (variables == null)
|
||||
continue;
|
||||
|
||||
currentIndex += variables.Data.Length;
|
||||
totalAmount = variables.TotalLength;
|
||||
|
||||
Variables.AddRange(variables.Data);
|
||||
|
||||
} while (Variables.Count < totalAmount);
|
||||
}
|
||||
|
||||
private async Task LaunchCreateAsync()
|
||||
{
|
||||
await DialogService.LaunchAsync<CreateVariableDialog>(parameters =>
|
||||
{
|
||||
parameters[nameof(CreateVariableDialog.Template)] = Template;
|
||||
parameters[nameof(CreateVariableDialog.OnSubmit)] = async () =>
|
||||
{
|
||||
await LazyLoader.ReloadAsync();
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
using LucideBlazor;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using Moonlight.Frontend.Models;
|
||||
|
||||
namespace MoonlightServers.Frontend.Implementations;
|
||||
|
||||
public class PermissionProvider : IPermissionProvider
|
||||
{
|
||||
public Task<PermissionCategory[]> GetPermissionsAsync()
|
||||
{
|
||||
return Task.FromResult<PermissionCategory[]>([
|
||||
new PermissionCategory("Demo", typeof(SparklesIcon), [
|
||||
new Permission("Permissions:Demo", "Demo", "Access to demo page")
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using LucideBlazor;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using Moonlight.Frontend.Models;
|
||||
|
||||
namespace MoonlightServers.Frontend.Implementations;
|
||||
|
||||
public sealed class SidebarProvider : ISidebarProvider
|
||||
{
|
||||
public Task<SidebarItem[]> GetItemsAsync()
|
||||
{
|
||||
return Task.FromResult<SidebarItem[]>([
|
||||
new SidebarItem()
|
||||
{
|
||||
Group = "Demo",
|
||||
Name = "Demo",
|
||||
IconType = typeof(SparklesIcon),
|
||||
Path = "/demo"
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
namespace MoonlightServers.Frontend.Infrastructure.Helpers;
|
||||
|
||||
internal static class Formatter
|
||||
{
|
||||
internal static string FormatSize(long bytes, double conversionStep = 1024)
|
||||
{
|
||||
if (bytes == 0) return "0 B";
|
||||
|
||||
string[] units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
||||
var unitIndex = 0;
|
||||
double size = bytes;
|
||||
|
||||
while (size >= conversionStep && unitIndex < units.Length - 1)
|
||||
{
|
||||
size /= conversionStep;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
var decimals = unitIndex == 0 ? 0 : 2;
|
||||
return $"{Math.Round(size, decimals)} {units[unitIndex]}";
|
||||
}
|
||||
|
||||
internal static string FormatDuration(TimeSpan timeSpan)
|
||||
{
|
||||
var abs = timeSpan.Duration(); // Handle negative timespans
|
||||
|
||||
if (abs.TotalSeconds < 1)
|
||||
return $"{abs.TotalMilliseconds:F0}ms";
|
||||
|
||||
if (abs.TotalMinutes < 1)
|
||||
return $"{abs.TotalSeconds:F1}s";
|
||||
|
||||
if (abs.TotalHours < 1)
|
||||
return $"{abs.Minutes}m {abs.Seconds}s";
|
||||
|
||||
if (abs.TotalDays < 1)
|
||||
return $"{abs.Hours}h {abs.Minutes}m";
|
||||
|
||||
if (abs.TotalDays < 365)
|
||||
return $"{abs.Days}d {abs.Hours}h";
|
||||
|
||||
var years = (int)(abs.TotalDays / 365);
|
||||
var days = abs.Days % 365;
|
||||
return days > 0 ? $"{years}y {days}d" : $"{years}y";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using LucideBlazor;
|
||||
using Moonlight.Frontend.Admin.Users.Shared;
|
||||
using Moonlight.Frontend.Infrastructure.Hooks;
|
||||
using MoonlightServers.Shared;
|
||||
|
||||
namespace MoonlightServers.Frontend.Infrastructure;
|
||||
|
||||
public class PermissionProvider : IPermissionProvider
|
||||
{
|
||||
public Task<PermissionCategory[]> GetPermissionsAsync()
|
||||
{
|
||||
return Task.FromResult<PermissionCategory[]>([
|
||||
new PermissionCategory("Servers - Nodes", typeof(ServerIcon), [
|
||||
new Permission(Permissions.Nodes.View, "View", "Viewing all nodes"),
|
||||
new Permission(Permissions.Nodes.Create, "Create", "Creating new nodes"),
|
||||
new Permission(Permissions.Nodes.Edit, "Edit", "Editing nodes"),
|
||||
new Permission(Permissions.Nodes.Delete, "Delete", "Deleting nodes"),
|
||||
]),
|
||||
new PermissionCategory("Servers - Servers", typeof(ContainerIcon), [
|
||||
new Permission(Permissions.Servers.View, "View", "Viewing all servers"),
|
||||
new Permission(Permissions.Servers.Create, "Create", "Creating new servers"),
|
||||
new Permission(Permissions.Servers.Edit, "Edit", "Editing servers"),
|
||||
new Permission(Permissions.Servers.Delete, "Delete", "Deleting servers"),
|
||||
]),
|
||||
new PermissionCategory("Servers - Templates", typeof(ContainerIcon), [
|
||||
new Permission(Permissions.Templates.View, "View", "Viewing all templates"),
|
||||
new Permission(Permissions.Templates.Create, "Create", "Creating new templates"),
|
||||
new Permission(Permissions.Templates.Edit, "Edit", "Editing templates"),
|
||||
new Permission(Permissions.Templates.Delete, "Delete", "Deleting templates"),
|
||||
])
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
@inherits Moonlight.Frontend.Infrastructure.Hooks.LayoutMiddlewareBase
|
||||
|
||||
@ChildContent
|
||||
|
||||
<script src="/_content/MoonlightServers.Frontend/realtimeChart.js"></script>
|
||||
29
MoonlightServers.Frontend/Infrastructure/SidebarProvider.cs
Normal file
29
MoonlightServers.Frontend/Infrastructure/SidebarProvider.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using LucideBlazor;
|
||||
using Moonlight.Frontend.Infrastructure.Hooks;
|
||||
using Moonlight.Frontend.Infrastructure.Models;
|
||||
using MoonlightServers.Shared;
|
||||
|
||||
namespace MoonlightServers.Frontend.Infrastructure;
|
||||
|
||||
public sealed class SidebarProvider : ISidebarProvider
|
||||
{
|
||||
public Task<SidebarItem[]> GetItemsAsync()
|
||||
{
|
||||
return Task.FromResult<SidebarItem[]>([
|
||||
new SidebarItem()
|
||||
{
|
||||
Name = "Servers",
|
||||
IconType = typeof(ServerIcon),
|
||||
Path = "/servers"
|
||||
},
|
||||
new SidebarItem()
|
||||
{
|
||||
Group = "Admin",
|
||||
Name = "Servers",
|
||||
IconType = typeof(ServerIcon),
|
||||
Path = "/admin/servers",
|
||||
Policy = Permissions.Servers.View
|
||||
}
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,17 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Nuget Package Settings">
|
||||
<Version>2.1.0</Version>
|
||||
<Title>MoonlightServers.Frontend</Title>
|
||||
<Authors>Moonlight Panel</Authors>
|
||||
<Description>Development package of MoonlightServers.Frontend</Description>
|
||||
<Copyright>Moonlight Panel</Copyright>
|
||||
<PackageProjectUrl>https://git.battlestati.one/Moonlight-Panel/Servers</PackageProjectUrl>
|
||||
<PackageLicenseUrl>https://git.battlestati.one/Moonlight-Panel/Servers/src/branch/v2.1/LICENSE</PackageLicenseUrl>
|
||||
<RepositoryUrl>https://git.battlestati.one/Moonlight-Panel/Servers</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SupportedPlatform Include="browser"/>
|
||||
@@ -13,10 +24,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moonlight.Frontend" Version="2.1.0"/>
|
||||
<PackageReference Include="ShadcnBlazor" Version="1.0.14"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\"/>
|
||||
<Folder Include="Client\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -25,8 +37,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Styles/*" Pack="true" PackagePath="Styles/"/>
|
||||
<None Include="Servers.Frontend.targets" Pack="true" PackagePath="build\Servers.Frontend.targets"/>
|
||||
<None Include="Servers.Frontend.targets" Pack="true" PackagePath="buildTransitive\Servers.Frontend.targets"/>
|
||||
<None Include="MoonlightServers.Frontend.targets" Pack="true" PackagePath="build\MoonlightServers.Frontend.targets" />
|
||||
<None Include="MoonlightServers.Frontend.targets" Pack="true" PackagePath="buildTransitive\MoonlightServers.Frontend.targets"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
15
MoonlightServers.Frontend/MoonlightServers.Frontend.targets
Normal file
15
MoonlightServers.Frontend/MoonlightServers.Frontend.targets
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<MoonlightServersCssClassDir Condition="'$(MoonlightServersCssClassDir)' == ''">
|
||||
$(MSBuildProjectDirectory)\bin\MoonlightServers
|
||||
</MoonlightServersCssClassDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="MoonlightServers_CopyContents" BeforeTargets="Build">
|
||||
<ItemGroup>
|
||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(Files)" DestinationFolder="$(MoonlightServersCssClassDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -1,15 +0,0 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ServersCssClassDir Condition="'$(ServersCssClassDir)' == ''">
|
||||
$(MSBuildProjectDirectory)\bin\Servers
|
||||
</ServersCssClassDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<Target Name="Servers_CopyContents" BeforeTargets="Build">
|
||||
<ItemGroup>
|
||||
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
|
||||
</ItemGroup>
|
||||
|
||||
<Copy SourceFiles="@(Files)" DestinationFolder="$(ServersCssClassDir)" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
</Project>
|
||||
93
MoonlightServers.Frontend/Shared/RealtimeChart.razor
Normal file
93
MoonlightServers.Frontend/Shared/RealtimeChart.razor
Normal file
@@ -0,0 +1,93 @@
|
||||
@using ShadcnBlazor.Cards
|
||||
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
@implements IAsyncDisposable
|
||||
|
||||
@typeparam T
|
||||
|
||||
<Card ClassName="py-0 overflow-hidden">
|
||||
<CardContent ClassName="@($"px-0 relative overflow-hidden {ClassName}")">
|
||||
<div class="absolute top-6 left-6 z-10">
|
||||
@if (!string.IsNullOrEmpty(Title))
|
||||
{
|
||||
<CardDescription>@Title</CardDescription>
|
||||
}
|
||||
<CardTitle ClassName="text-lg">
|
||||
@if (CurrentValue != null)
|
||||
{
|
||||
@DisplayField.Invoke(CurrentValue)
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>-</span>
|
||||
}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<canvas id="@Identifier" class="absolute block rounded-xl -left-5 -right-5 top-0 -bottom-2 w-[calc(100%+30px)]! h-[calc(100%+8px)]!">
|
||||
</canvas>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@code
|
||||
{
|
||||
[Parameter] public IEnumerable<T>? DefaultItems { get; set; }
|
||||
[Parameter] public Func<T, string> DisplayField { get; set; }
|
||||
[Parameter] public Func<T, double> ValueField { get; set; }
|
||||
[Parameter] public string Title { get; set; }
|
||||
[Parameter] public int Min { get; set; } = 0;
|
||||
[Parameter] public int Max { get; set; } = 100;
|
||||
[Parameter] public int VisibleDataPoints { get; set; } = 30;
|
||||
|
||||
[Parameter] public string ClassName { get; set; }
|
||||
|
||||
private string Identifier;
|
||||
private T? CurrentValue;
|
||||
private int Counter;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
Identifier = $"realtimeChart{GetHashCode()}";
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!firstRender) return;
|
||||
|
||||
var items = DefaultItems?.ToArray() ?? [];
|
||||
|
||||
var labels = items.Select(x =>
|
||||
{
|
||||
Counter++;
|
||||
return Counter.ToString();
|
||||
});
|
||||
|
||||
var dataPoints = items.Select(ValueField);
|
||||
|
||||
await JsRuntime.InvokeVoidAsync(
|
||||
"moonlightServersRealtimeChart.init",
|
||||
Identifier,
|
||||
Identifier,
|
||||
VisibleDataPoints,
|
||||
Min,
|
||||
Max,
|
||||
labels,
|
||||
dataPoints
|
||||
);
|
||||
}
|
||||
|
||||
public async Task PushAsync(T value)
|
||||
{
|
||||
Counter++;
|
||||
var label = Counter.ToString();
|
||||
var dataPoint = ValueField.Invoke(value);
|
||||
|
||||
CurrentValue = value;
|
||||
|
||||
await JsRuntime.InvokeVoidAsync("moonlightServersRealtimeChart.pushValue", Identifier, label, dataPoint);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
=> await JsRuntime.InvokeVoidAsync("moonlightServersRealtimeChart.destroy", Identifier);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Moonlight.Frontend;
|
||||
using Moonlight.Frontend.Configuration;
|
||||
using Moonlight.Frontend.Interfaces;
|
||||
using MoonlightServers.Frontend.Implementations;
|
||||
using Moonlight.Frontend.Infrastructure.Configuration;
|
||||
using Moonlight.Frontend.Infrastructure.Hooks;
|
||||
using MoonlightServers.Frontend.Infrastructure;
|
||||
using SimplePlugin.Abstractions;
|
||||
|
||||
namespace MoonlightServers.Frontend;
|
||||
@@ -20,6 +20,11 @@ public sealed class Startup : MoonlightPlugin
|
||||
{
|
||||
options.Assemblies.Add(typeof(Startup).Assembly);
|
||||
});
|
||||
|
||||
builder.Services.Configure<LayoutMiddlewareOptions>(options =>
|
||||
{
|
||||
options.Add<ScriptImports>();
|
||||
});
|
||||
}
|
||||
|
||||
public override void PostBuild(WebAssemblyHost application)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
THIS WILL BE AUTOGENERATED DURING PACKAGE BUILD
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user