Merge branch 'feat/ContainerHelper' into v2.1
Some checks failed
Dev Publish: Nuget / Publish Dev Packages (push) Failing after 31s

# Conflicts:
#	Moonlight.Api/Services/ApplicationService.cs
#	Moonlight.Api/Startup/Startup.Base.cs
#	Moonlight.Shared/Http/SerializationContext.cs
This commit is contained in:
2026-02-09 08:18:56 +01:00
72 changed files with 1416 additions and 606 deletions

View File

@@ -0,0 +1,7 @@
namespace Moonlight.Api.Configuration;
public class ContainerHelperOptions
{
public bool IsEnabled { get; set; }
public string Url { get; set; } = "http://helper:8080";
}

View File

@@ -6,9 +6,9 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.ApiKeys; using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Mappers;
using Moonlight.Api.Services;
using Moonlight.Shared.Http.Events;
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin;
[ApiController]
[Route("api/admin/ch")]
public class ContainerHelperController : Controller
{
private readonly ContainerHelperService ContainerHelperService;
private readonly IOptions<ContainerHelperOptions> Options;
public ContainerHelperController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
{
ContainerHelperService = containerHelperService;
Options = options;
}
[HttpGet("status")]
public async Task<ActionResult<ContainerHelperStatusDto>> GetStatusAsync()
{
if (!Options.Value.IsEnabled)
return new ContainerHelperStatusDto(false, false);
var status = await ContainerHelperService.CheckConnectionAsync();
return new ContainerHelperStatusDto(true, status);
}
[HttpPost("rebuild")]
public Task<IResult> RebuildAsync([FromBody] RequestRebuildDto request)
{
var result = ContainerHelperService.RebuildAsync(request.NoBuildCache);
var mappedResult = result.Select(ContainerHelperMapper.ToDto);
return Task.FromResult<IResult>(
TypedResults.ServerSentEvents(mappedResult)
);
}
[HttpPost("version")]
public async Task<ActionResult> SetVersionAsync([FromBody] SetVersionDto request)
{
await ContainerHelperService.SetVersionAsync(request.Version);
return NoContent();
}
}

View File

@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Users; using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -6,7 +6,7 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Roles; using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;

View File

@@ -7,9 +7,9 @@ using Moonlight.Api.Mappers;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Themes; using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Themes; using Moonlight.Shared.Http.Responses.Admin.Themes;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -6,9 +6,9 @@ using Moonlight.Api.Database.Entities;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Shared; using Moonlight.Shared;
using Moonlight.Shared.Http.Requests; using Moonlight.Shared.Http.Requests;
using Moonlight.Shared.Http.Requests.Users; using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Users; using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Shared.Http.Responses.Auth; using Moonlight.Shared.Http.Responses.Admin.Auth;
namespace Moonlight.Api.Http.Controllers; namespace Moonlight.Api.Http.Controllers;

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Mappers; using Moonlight.Api.Mappers;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using Moonlight.Shared.Http.Responses.Frontend; using Moonlight.Shared.Http.Responses.Admin.Frontend;
namespace Moonlight.Api.Http.Controllers; namespace Moonlight.Api.Http.Controllers;

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Moonlight.Api.Http.Controllers;
[ApiController]
[Route("api/ping")]
public class PingController : Controller
{
[HttpGet]
[AllowAnonymous]
public IActionResult Get() => Ok("Pong");
}

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Moonlight.Api.Http.Services.ContainerHelper.Events;
public struct RebuildEventDto
{
[JsonPropertyName("type")]
public RebuildEventType Type { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; }
}
public enum RebuildEventType
{
Log = 0,
Failed = 1,
Succeeded = 2,
Step = 3
}

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Api.Http.Services.ContainerHelper;
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; }
}

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record RequestRebuildDto(bool NoBuildCache);

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Api.Http.Services.ContainerHelper.Requests;
public record SetVersionDto(string Version);

View File

@@ -0,0 +1,29 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Moonlight.Api.Http.Services.ContainerHelper.Events;
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
namespace Moonlight.Api.Http.Services.ContainerHelper;
[JsonSerializable(typeof(SetVersionDto))]
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(RebuildEventDto))]
[JsonSerializable(typeof(RequestRebuildDto))]
public partial class SerializationContext : JsonSerializerContext
{
private static JsonSerializerOptions? InternalTunedOptions;
public static JsonSerializerOptions TunedOptions
{
get
{
if (InternalTunedOptions != null)
return InternalTunedOptions;
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
return InternalTunedOptions;
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.ApiKeys; using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -0,0 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Events;
using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.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 ContainerHelperMapper
{
public static partial RebuildEventDto ToDto(Http.Services.ContainerHelper.Events.RebuildEventDto rebuildEventDto);
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Models; using Moonlight.Api.Models;
using Moonlight.Shared.Http.Responses.Frontend; using Moonlight.Shared.Http.Responses.Admin.Frontend;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Roles; using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Themes; using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses.Themes; using Moonlight.Shared.Http.Responses.Admin.Themes;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses.Users;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Api.Mappers; namespace Moonlight.Api.Mappers;

View File

@@ -35,4 +35,8 @@
<Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible> <Visible Condition="'%(NuGetItemType)' == 'Content'">false</Visible>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Http\Services\ContainerHelper\Responses\" />
</ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,117 @@
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
using Moonlight.Api.Http.Services.ContainerHelper;
using Moonlight.Api.Http.Services.ContainerHelper.Requests;
using Moonlight.Api.Http.Services.ContainerHelper.Events;
namespace Moonlight.Api.Services;
public class ContainerHelperService
{
private readonly IHttpClientFactory HttpClientFactory;
public ContainerHelperService(IHttpClientFactory httpClientFactory)
{
HttpClientFactory = httpClientFactory;
}
public async Task<bool> CheckConnectionAsync()
{
var client = HttpClientFactory.CreateClient("ContainerHelper");
try
{
var response = await client.GetAsync("api/ping");
response.EnsureSuccessStatusCode();
return true;
}
catch (Exception)
{
return false;
}
}
public async IAsyncEnumerable<RebuildEventDto> RebuildAsync(bool noBuildCache)
{
var client = HttpClientFactory.CreateClient("ContainerHelper");
var request = new HttpRequestMessage(HttpMethod.Post, "api/rebuild");
request.Content = JsonContent.Create(
new RequestRebuildDto(noBuildCache),
null,
SerializationContext.TunedOptions
);
var response = await client.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead
);
if (!response.IsSuccessStatusCode)
{
var responseText = await response.Content.ReadAsStringAsync();
yield return new RebuildEventDto()
{
Type = RebuildEventType.Failed,
Data = responseText
};
yield break;
}
await using var responseStream = await response.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(responseStream);
do
{
var line = await streamReader.ReadLineAsync();
if (line == null)
break;
if (string.IsNullOrWhiteSpace(line))
continue;
var data = line.Trim("data: ");
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.TunedOptions);
yield return deserializedData;
// Exit if service will go down for a clean exit
if (deserializedData is { Type: RebuildEventType.Step, Data: "ServiceDown" })
yield break;
} while (true);
yield return new RebuildEventDto()
{
Type = RebuildEventType.Succeeded,
Data = string.Empty
};
}
public async Task SetVersionAsync(string version)
{
var client = HttpClientFactory.CreateClient("ContainerHelper");
var response = await client.PostAsJsonAsync(
"api/configuration/version",
new SetVersionDto(version),
SerializationContext.TunedOptions
);
if (response.IsSuccessStatusCode)
return;
var problemDetails =
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions);
if (problemDetails == null)
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");
throw new HttpRequestException($"Failed to set version: {problemDetails.Detail ?? problemDetails.Title}");
}
}

View File

@@ -9,7 +9,7 @@ using Moonlight.Api.Helpers;
using Moonlight.Api.Implementations; using Moonlight.Api.Implementations;
using Moonlight.Api.Interfaces; using Moonlight.Api.Interfaces;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using SessionOptions = Moonlight.Api.Configuration.SessionOptions; using SessionOptions = Microsoft.AspNetCore.Builder.SessionOptions;
namespace Moonlight.Api.Startup; namespace Moonlight.Api.Startup;
@@ -38,11 +38,20 @@ public partial class Startup
builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend"); builder.Services.AddOptions<FrontendOptions>().BindConfiguration("Moonlight:Frontend");
builder.Services.AddScoped<FrontendService>(); builder.Services.AddScoped<FrontendService>();
builder.Services.AddHttpClient(); builder.Services.AddHttpClient();
builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version"); builder.Services.AddOptions<VersionOptions>().BindConfiguration("Moonlight:Version");
builder.Services.AddSingleton<VersionService>(); builder.Services.AddSingleton<VersionService>();
builder.Services.AddOptions<ContainerHelperOptions>().BindConfiguration("Moonlight:ContainerHelper");
builder.Services.AddSingleton<ContainerHelperService>();
builder.Services.AddHttpClient("ContainerHelper", (provider, client) =>
{
var options = provider.GetRequiredService<IOptions<ContainerHelperOptions>>();
client.BaseAddress = new Uri(options.Value.IsEnabled ? options.Value.Url : "http://you-should-fail.invalid");
});
} }
private static void UseBase(WebApplication application) private static void UseBase(WebApplication application)

View File

@@ -0,0 +1,30 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.Forms;
using Moonlight.Shared.Http.Responses;
namespace Moonlight.Frontend.Helpers;
public static class ProblemDetailsHelper
{
public static async Task HandleProblemDetailsAsync(HttpResponseMessage response, object model, ValidationMessageStore validationMessageStore)
{
var problemDetails = await response.Content.ReadFromJsonAsync<ProblemDetails>();
if (problemDetails == null)
response.EnsureSuccessStatusCode(); // Trigger exception when unable to parse
else
{
if(!string.IsNullOrEmpty(problemDetails.Detail))
validationMessageStore.Add(new FieldIdentifier(model, string.Empty), problemDetails.Detail);
if (problemDetails.Errors != null)
{
foreach (var error in problemDetails.Errors)
{
foreach (var message in error.Value)
validationMessageStore.Add(new FieldIdentifier(model, error.Key), message);
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.ApiKeys; using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Frontend.Mappers; namespace Moonlight.Frontend.Mappers;

View File

@@ -1,5 +1,5 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Roles; using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;

View File

@@ -1,6 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Themes; using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Responses.Themes; using Moonlight.Shared.Http.Responses.Admin.Themes;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
namespace Moonlight.Frontend.Mappers; namespace Moonlight.Frontend.Mappers;

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses.Admin.Users;
using Riok.Mapperly.Abstractions; using Riok.Mapperly.Abstractions;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Frontend.Mappers; namespace Moonlight.Frontend.Mappers;

View File

@@ -24,8 +24,8 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/> <PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
<PackageReference Include="ShadcnBlazor" Version="1.0.9" /> <PackageReference Include="ShadcnBlazor" Version="1.0.11" />
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.9" /> <PackageReference Include="ShadcnBlazor.Extras" Version="1.0.11" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -3,7 +3,7 @@ using System.Net.Http.Json;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Moonlight.Shared.Http.Responses.Auth; using Moonlight.Shared.Http.Responses.Admin.Auth;
namespace Moonlight.Frontend.Services; namespace Moonlight.Frontend.Services;

View File

@@ -1,13 +1,18 @@
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.Helpers
@using Moonlight.Shared.Http.Requests.ApiKeys @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle>Create new API key</DialogTitle> <DialogTitle>Create new API key</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -15,56 +20,74 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6">
<FormValidationSummary />
<div class="grid gap-2"> <FieldGroup>
<Label for="keyName">Name</Label> <DataAnnotationsValidator/>
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" /> <FormValidationSummary/>
</div>
<FieldSet>
<div class="grid gap-2"> <Field>
<Label for="keyDescription">Description</Label> <FieldLabel for="keyName">Name</FieldLabel>
<textarea <TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
@bind="Request.Description" </Field>
id="keyDescription" <Field>
maxlength="100" <FieldLabel for="keyDescription">Description</FieldLabel>
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" <TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
placeholder="What this key is for"> </Field>
<Field>
</textarea> <FieldLabel>Permissions</FieldLabel>
</div> <FieldContent>
<PermissionSelector Permissions="Permissions"/>
<div class="grid gap-2"> </FieldContent>
<Label>Permissions</Label> </Field>
<PermissionSelector Permissions="Permissions" /> </FieldSet>
</div> <Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
</div> <SubmitButton>Save changes</SubmitButton>
</FormHandler> </Field>
</FieldGroup>
<DialogFooter ClassName="justify-end gap-x-1"> </EnhancedEditForm>
<WButtom OnClick="() => FormHandler.SubmitAsync()">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<CreateApiKeyDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
private CreateApiKeyDto Request; private CreateApiKeyDto Request;
private FormHandler FormHandler;
private List<string> Permissions = new(); private List<string> Permissions = new();
protected override void OnInitialized() protected override void OnInitialized()
{ {
Request = new(); Request = new()
{
Permissions = []
};
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await OnSubmit.Invoke(Request);
var response = await HttpClient.PostAsJsonAsync(
"/api/admin/apiKeys",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"API Key creation",
$"Successfully created API key {Request.Name}"
);
await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,14 +1,17 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Roles @using Moonlight.Shared.Http.Requests.Admin.Roles
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Create new role Create new role
@@ -18,49 +21,43 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6"> <FieldGroup>
<FormValidationSummary/> <FormValidationSummary/>
<DataAnnotationsValidator/>
<div class="grid gap-2"> <FieldSet>
<Label for="roleName">Name</Label> <Field>
<InputField <FieldLabel for="roleName">Name</FieldLabel>
@bind-Value="Request.Name" <TextInputField
id="roleName" @bind-Value="Request.Name"
placeholder="My fancy role"/> id="roleName"
</div> placeholder="My fancy role"/>
</Field>
<div class="grid gap-2"> <Field>
<Label for="roleDescription">Description</Label> <FieldLabel for="keyDescription">Description</FieldLabel>
<textarea <TextareaInputField @bind-Value="Request.Description" id="keyDescription"
@bind="Request.Description" placeholder="Describe what the role should be used for"/>
id="roleDescription" </Field>
maxlength="100" <Field>
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" <FieldLabel>Permissions</FieldLabel>
placeholder="Describe what the role should be used for"> <FieldContent>
<PermissionSelector Permissions="Permissions"/>
</textarea> </FieldContent>
</div> </Field>
</FieldSet>
<div class="grid gap-2"> <Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
<Label>Permissions</Label> <SubmitButton>Save changes</SubmitButton>
<PermissionSelector Permissions="Permissions" /> </Field>
</div> </FieldGroup>
</div> </EnhancedEditForm>
</FormHandler>
<DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<CreateRoleDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
private CreateRoleDto Request; private CreateRoleDto Request;
private List<string> Permissions; private List<string> Permissions;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -68,19 +65,31 @@
{ {
Permissions = [] Permissions = []
}; };
Permissions = new(); Permissions = new();
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await FormHandler.SubmitAsync();
} var response = await HttpClient.PostAsJsonAsync(
"api/admin/roles",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
private async Task OnSubmitAsync() await ToastService.SuccessAsync("Role creation", $"Role {Request.Name} has been successfully created");
{
await OnSubmit.Invoke(Request); await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,12 +1,17 @@
@using Moonlight.Shared.Http.Requests.Users @using Moonlight.Frontend.Helpers
@using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Create new user Create new user
@@ -16,50 +21,67 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6">
<FieldGroup>
<FormValidationSummary/> <FormValidationSummary/>
<DataAnnotationsValidator />
<div class="grid gap-2"> <FieldSet>
<Label for="username">Username</Label> <Field>
<InputField <FieldLabel for="username">Username</FieldLabel>
@bind-Value="Request.Username" <TextInputField
id="username" @bind-Value="Request.Username"
placeholder="Name of the user"/> id="username"
</div> placeholder="Name of the user"/>
</Field>
<div class="grid gap-2"> <Field>
<Label for="emailAddress">Email Address</Label> <FieldLabel for="emailAddress">Email Address</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Email" @bind-Value="Request.Email"
id="emailAddress" id="emailAddress"
Type="email" placeholder="email@of.user"/>
placeholder="email@of.user"/> </Field>
</div> </FieldSet>
</div> <Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
</FormHandler> <SubmitButton>Save changes</SubmitButton>
</Field>
<DialogFooter ClassName="justify-end gap-x-1"> </FieldGroup>
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom> </EnhancedEditForm>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<CreateUserDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnCompleted { get; set; }
private CreateUserDto Request; private CreateUserDto Request;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
Request = new(); Request = new();
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
await OnSubmit.Invoke(Request); var response = await HttpClient.PostAsJsonAsync(
"/api/admin/users",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"User creation",
$"Successfully created user {Request.Username}"
);
await OnCompleted.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,7 +1,7 @@
@using LucideBlazor @using LucideBlazor
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using Moonlight.Shared.Http.Responses.Users @using Moonlight.Shared.Http.Responses.Admin.Users
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@@ -32,9 +32,9 @@
SearchPlaceholder="Search user" SearchPlaceholder="Search user"
ValueSelector="dto => dto.Username" ValueSelector="dto => dto.Username"
Source="LoadUsersAsync"/> Source="LoadUsersAsync"/>
<WButtom OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon"> <WButton OnClick="AddAsync" Variant="ButtonVariant.Outline" Size="ButtonSize.Icon">
<PlusIcon/> <PlusIcon/>
</WButtom> </WButton>
</div> </div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
@@ -50,9 +50,9 @@
<CellTemplate> <CellTemplate>
<TableCell> <TableCell>
<div class="flex justify-end me-1.5"> <div class="flex justify-end me-1.5">
<WButtom OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon"> <WButton OnClick="_ => RemoveAsync(context)" Variant="ButtonVariant.Destructive" Size="ButtonSize.Icon">
<TrashIcon/> <TrashIcon/>
</WButtom> </WButton>
</div> </div>
</TableCell> </TableCell>
</CellTemplate> </CellTemplate>

View File

@@ -1,15 +1,19 @@
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.ApiKeys @using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.ApiKeys @using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle>Update API key</DialogTitle> <DialogTitle>Update API key</DialogTitle>
<DialogDescription> <DialogDescription>
@@ -17,45 +21,39 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6"> <FieldGroup>
<FormValidationSummary /> <FormValidationSummary/>
<DataAnnotationsValidator/>
<div class="grid gap-2"> <FieldSet>
<Label for="keyName">Name</Label> <Field>
<InputField @bind-Value="Request.Name" id="keyName" placeholder="My API key" /> <FieldLabel for="keyName">Name</FieldLabel>
</div> <TextInputField @bind-Value="Request.Name" id="keyName" placeholder="My API key"/>
</Field>
<div class="grid gap-2"> <Field>
<Label for="keyDescription">Description</Label> <FieldLabel for="keyDescription">Description</FieldLabel>
<textarea <TextareaInputField @bind-Value="Request.Description" id="keyDescription" placeholder="What this key is for"/>
@bind="Request.Description" </Field>
id="keyDescription" <Field>
maxlength="100" <FieldLabel>Permissions</FieldLabel>
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" <FieldContent>
placeholder="What this key is for"> <PermissionSelector Permissions="Permissions"/>
</FieldContent>
</textarea> </Field>
</div> </FieldSet>
<Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
<div class="grid gap-2"> <SubmitButton>Save changes</SubmitButton>
<Label>Permissions</Label> </Field>
<PermissionSelector Permissions="Permissions" /> </FieldGroup>
</div> </EnhancedEditForm>
</div>
</FormHandler>
<DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<UpdateApiKeyDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public ApiKeyDto Key { get; set; } [Parameter] public ApiKeyDto Key { get; set; }
private UpdateApiKeyDto Request; private UpdateApiKeyDto Request;
private FormHandler FormHandler;
private List<string> Permissions = new(); private List<string> Permissions = new();
protected override void OnInitialized() protected override void OnInitialized()
@@ -64,10 +62,30 @@
Permissions = Key.Permissions.ToList(); Permissions = Key.Permissions.ToList();
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await OnSubmit.Invoke(Request);
var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/apiKeys/{Key.Id}",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"API Key update",
$"Successfully updated API key {Request.Name}"
);
await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,37 +1,38 @@
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@using System.Text.Json
@using LucideBlazor @using LucideBlazor
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Events
@using Moonlight.Shared.Http.Requests.Admin.ContainerHelper
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Progresses @using ShadcnBlazor.Progresses
@using ShadcnBlazor.Spinners @using ShadcnBlazor.Spinners
@inject AlertDialogService AlertService @inject HttpClient HttpClient
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Updating... Updating instance to @Version...
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<div class="text-base flex flex-col p-2 gap-y-0.5"> <div class="grid grid-cols-1 xl:grid-cols-2 w-full gap-5">
@for (var i = 0; i < Steps.Length; i++) <div class="text-base flex flex-col p-2 gap-y-1">
{ @for (var i = 0; i < Steps.Length; i++)
if (CurrentStep == i)
{ {
<div class="flex flex-row items-center gap-x-2"> if (CurrentStep == i)
<Spinner ClassName="size-4" />
<span>
@Steps[i]
</span>
</div>
}
else
{
if (i < CurrentStep)
{ {
<div class="flex flex-row items-center gap-x-2"> <div class="flex flex-row items-center gap-x-1">
<CheckIcon ClassName="text-green-500 size-4" /> @if (IsFailed)
{
<CircleXIcon ClassName="text-red-500 size-5"/>
}
else
{
<Spinner ClassName="size-5"/>
}
<span> <span>
@Steps[i] @Steps[i]
</span> </span>
@@ -39,81 +40,205 @@
} }
else else
{ {
<div class="text-muted-foreground flex flex-row items-center gap-x-2"> if (i < CurrentStep)
<span class="size-4"></span> {
@Steps[i] <div class="flex flex-row items-center gap-x-1 text-muted-foreground">
</div> <CircleCheckIcon ClassName="text-green-500 size-5"/>
<span>
@Steps[i]
</span>
</div>
}
else
{
<div class="text-muted-foreground flex flex-row items-center gap-x-1">
<span class="size-5"></span>
@Steps[i]
</div>
}
} }
} }
} </div>
<div class="bg-black text-white rounded-lg font-mono h-96 flex flex-col-reverse overflow-auto p-3 scrollbar-thin">
@for (var i = LogLines.Count - 1; i >= 0; i--)
{
<div>
@LogLines[i]
</div>
}
</div>
</div> </div>
<DialogFooter> @if (CurrentStep == Steps.Length || IsFailed)
<Progress Value="@Progress"></Progress> {
</DialogFooter> <DialogFooter ClassName="justify-end">
<Button Variant="ButtonVariant.Outline" @onclick="CloseAsync">Close</Button>
</DialogFooter>
}
else
{
<DialogFooter>
<Progress ClassName="my-1" Value="@Progress"></Progress>
</DialogFooter>
}
@code @code
{ {
private int Progress = 0; [Parameter] public string Version { get; set; }
[Parameter] public bool NoBuildCache { get; set; }
private bool IsFailed;
private int Progress;
private int CurrentStep; private int CurrentStep;
private string[] Steps = private readonly string[] Steps =
[ [
"Preparing", "Checking", // 0
"Updating configuration files", "Updating configuration files", // 1
"Building docker image", "Starting rebuild task", // 2
"Redeploying container instance", "Building docker image", // 3
"Waiting for container instance to start up", "Redeploying container instance", // 4
"Update complete" "Waiting for container instance to start up", // 5
"Update complete" // 6
]; ];
private readonly List<string?> LogLines = new();
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (!firstRender) if (!firstRender)
return; return;
// Checking
CurrentStep = 0; CurrentStep = 0;
Progress = 0; Progress = 0;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Task.Delay(2000); await Task.Delay(2000);
// Update configuration
CurrentStep = 1; CurrentStep = 1;
Progress = 20; Progress = 20;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Task.Delay(6000); await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
{
Version = Version
}, SerializationContext.TunedOptions);
// Starting rebuild task
CurrentStep = 2; CurrentStep = 2;
Progress = 40; Progress = 30;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Task.Delay(2000); var request = new HttpRequestMessage(HttpMethod.Post, "api/admin/ch/rebuild");
CurrentStep = 3; request.Content = JsonContent.Create(
Progress = 60; new RequestRebuildDto(NoBuildCache),
await InvokeAsync(StateHasChanged); null,
SerializationContext.TunedOptions
await Task.Delay(4000);
CurrentStep = 4;
Progress = 80;
await InvokeAsync(StateHasChanged);
await Task.Delay(4000);
CurrentStep = 5;
Progress = 100;
await InvokeAsync(StateHasChanged);
await Task.Delay(1000);
await AlertService.SuccessAsync(
"Update completed",
"Update successfully completed. Please refresh the page to load new frontend changes"
); );
await CloseAsync(); var response = await HttpClient.SendAsync(
request,
HttpCompletionOption.ResponseHeadersRead
);
await using var responseStream = await response.Content.ReadAsStreamAsync();
using var streamReader = new StreamReader(responseStream);
do
{
try
{
var line = await streamReader.ReadLineAsync();
if (line == null)
break;
if (string.IsNullOrWhiteSpace(line))
continue;
var data = line.Trim("data: ");
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, Constants.SerializerOptions);
switch (deserializedData.Type)
{
case RebuildEventType.Log:
LogLines.Add(deserializedData.Data);
break;
case RebuildEventType.Step:
switch (deserializedData.Data)
{
case "BuildImage":
// Building docker image
CurrentStep = 3;
Progress = 40;
await InvokeAsync(StateHasChanged);
break;
case "ServiceDown":
// Redeploying container instance
CurrentStep = 4;
Progress = 60;
await InvokeAsync(StateHasChanged);
break;
}
break;
case RebuildEventType.Failed:
IsFailed = true;
await InvokeAsync(StateHasChanged);
return;
}
await InvokeAsync(StateHasChanged);
}
catch (Exception)
{
break;
}
} while (true);
// Waiting for container instance to start up
CurrentStep = 5;
Progress = 90;
await InvokeAsync(StateHasChanged);
// Wait some time for instance to shut down
await Task.Delay(TimeSpan.FromSeconds(5));
// Ping instance until its reachable again
while (true)
{
try
{
await HttpClient.GetStringAsync("api/ping");
break;
}
catch (Exception)
{
// Ignored
}
await Task.Delay(3000);
}
// Update complete
CurrentStep = 7;
Progress = 100;
await InvokeAsync(StateHasChanged);
} }
} }

View File

@@ -1,16 +1,19 @@
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http.Requests.Roles @using Moonlight.Shared.Http.Requests.Admin.Roles
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Update @Role.Name Update @Role.Name
@@ -20,50 +23,44 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="OnSubmitAsync"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6"> <FieldGroup>
<FormValidationSummary/> <FormValidationSummary/>
<DataAnnotationsValidator/>
<div class="grid gap-2"> <FieldSet>
<Label for="roleName">Name</Label> <Field>
<InputField <FieldLabel for="roleName">Name</FieldLabel>
@bind-Value="Request.Name" <TextInputField
id="roleName" @bind-Value="Request.Name"
placeholder="My fancy role"/> id="roleName"
</div> placeholder="My fancy role"/>
</Field>
<div class="grid gap-2"> <Field>
<Label for="roleDescription">Description</Label> <FieldLabel for="keyDescription">Description</FieldLabel>
<textarea <TextareaInputField @bind-Value="Request.Description" id="keyDescription"
@bind="Request.Description" placeholder="Describe what the role should be used for"/>
id="roleDescription" </Field>
maxlength="100" <Field>
class="border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" <FieldLabel>Permissions</FieldLabel>
placeholder="Describe what the role should be used for"> <FieldContent>
<PermissionSelector Permissions="Permissions"/>
</textarea> </FieldContent>
</div> </Field>
</FieldSet>
<div class="grid gap-2"> <Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
<Label>Permissions</Label> <SubmitButton>Save changes</SubmitButton>
<PermissionSelector Permissions="Permissions" /> </Field>
</div> </FieldGroup>
</div> </EnhancedEditForm>
</FormHandler>
<DialogFooter ClassName="justify-end gap-x-1">
<WButtom OnClick="SubmitAsync">Save changes</WButtom>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<UpdateRoleDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnSubmit { get; set; }
[Parameter] public RoleDto Role { get; set; } [Parameter] public RoleDto Role { get; set; }
private UpdateRoleDto Request; private UpdateRoleDto Request;
private List<string> Permissions; private List<string> Permissions;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
@@ -71,15 +68,27 @@
Permissions = Role.Permissions.ToList(); Permissions = Role.Permissions.ToList();
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.Permissions = Permissions.ToArray(); Request.Permissions = Permissions.ToArray();
await FormHandler.SubmitAsync();
} var response = await HttpClient.PatchAsJsonAsync(
$"api/admin/roles/{Role.Id}",
Request,
Constants.SerializerOptions
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
private async Task OnSubmitAsync() await ToastService.SuccessAsync("Role update", $"Role {Request.Name} has been successfully updated");
{
await OnSubmit.Invoke(Request); await OnSubmit.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -1,14 +1,19 @@
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Helpers
@using Moonlight.Shared.Http.Requests.Users @using Moonlight.Frontend.Mappers
@using Moonlight.Shared.Http.Responses.Users @using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.Users
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Labels
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase @inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject ToastService ToastService
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
Update @User.Username Update @User.Username
@@ -18,51 +23,66 @@
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<FormHandler @ref="FormHandler" Model="Request" OnValidSubmit="SubmitAsync"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync">
<div class="flex flex-col gap-6"> <FieldGroup>
<FormValidationSummary/> <FormValidationSummary/>
<DataAnnotationsValidator/>
<div class="grid gap-2"> <FieldSet>
<Label for="username">Username</Label> <Field>
<InputField <FieldLabel for="username">Username</FieldLabel>
@bind-Value="Request.Username" <TextInputField
id="username" @bind-Value="Request.Username"
placeholder="Name of the user"/> id="username"
</div> placeholder="Name of the user"/>
</Field>
<div class="grid gap-2"> <Field>
<Label for="emailAddress">Email Address</Label> <FieldLabel for="emailAddress">Email Address</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Email" @bind-Value="Request.Email"
id="emailAddress" id="emailAddress"
Type="email" placeholder="email@of.user"/>
placeholder="email@of.user"/> </Field>
</div> </FieldSet>
</div> <Field Orientation="FieldOrientation.Horizontal" ClassName="justify-end">
</FormHandler> <SubmitButton>Save changes</SubmitButton>
</Field>
<DialogFooter ClassName="justify-end gap-x-1"> </FieldGroup>
<WButtom OnClick="_ => FormHandler.SubmitAsync()">Save changes</WButtom> </EnhancedEditForm>
</DialogFooter>
@code @code
{ {
[Parameter] public Func<UpdateUserDto, Task> OnSubmit { get; set; } [Parameter] public Func<Task> OnCompleted { get; set; }
[Parameter] public UserDto User { get; set; } [Parameter] public UserDto User { get; set; }
private UpdateUserDto Request; private UpdateUserDto Request;
private FormHandler FormHandler;
protected override void OnInitialized() protected override void OnInitialized()
{ {
Request = UserMapper.ToUpdate(User); Request = UserMapper.ToUpdate(User);
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
await OnSubmit.Invoke(Request); var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/users/{User.Id}",
Request
);
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync(
"User update",
$"Successfully updated user {Request.Username}"
);
await OnCompleted.Invoke();
await CloseAsync(); await CloseAsync();
return true;
} }
} }

View File

@@ -133,7 +133,11 @@
{ {
<CardTitle ClassName="text-lg text-primary">Update available</CardTitle> <CardTitle ClassName="text-lg text-primary">Update available</CardTitle>
<CardAction ClassName="self-center"> <CardAction ClassName="self-center">
<Button @onclick="LaunchUpdateModalAsync">Update</Button> <Button>
<Slot>
<a href="/admin/system?tab=instance" @attributes="context">Update</a>
</Slot>
</Button>
</CardAction> </CardAction>
} }
</CardHeader> </CardHeader>
@@ -156,9 +160,4 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task LaunchUpdateModalAsync() => await DialogService.LaunchAsync<UpdateInstanceModal>(onConfigure: model =>
{
model.ShowCloseButton = false;
});
} }

View File

@@ -1,12 +1,11 @@
@using Moonlight.Shared.Http.Requests.ApiKeys @using LucideBlazor
@using Moonlight.Shared.Http.Responses.ApiKeys
@using LucideBlazor
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dropdowns @using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Extras.AlertDialogs @using ShadcnBlazor.Extras.AlertDialogs
@@ -123,19 +122,8 @@
{ {
await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters => await DialogService.LaunchAsync<CreateApiKeyDialog>(parameters =>
{ {
parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async (CreateApiKeyDto dto) => parameters[nameof(CreateApiKeyDialog.OnSubmit)] = async () =>
{ {
await HttpClient.PostAsJsonAsync(
"/api/admin/apiKeys",
dto,
Constants.SerializerOptions
);
await ToastService.SuccessAsync(
"API Key creation",
$"Successfully created API key {dto.Name}"
);
await Grid.RefreshAsync(); await Grid.RefreshAsync();
}; };
}); });
@@ -146,19 +134,8 @@
await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters => await DialogService.LaunchAsync<UpdateApiKeyDialog>(parameters =>
{ {
parameters[nameof(UpdateApiKeyDialog.Key)] = key; parameters[nameof(UpdateApiKeyDialog.Key)] = key;
parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async (UpdateApiKeyDto dto) => parameters[nameof(UpdateApiKeyDialog.OnSubmit)] = async () =>
{ {
await HttpClient.PatchAsJsonAsync(
$"/api/admin/apiKeys/{key.Id}",
dto,
Constants.SerializerOptions
);
await ToastService.SuccessAsync(
"API Key update",
$"Successfully updated API key {dto.Name}"
);
await Grid.RefreshAsync(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -44,10 +44,10 @@
</Alert> </Alert>
</CardContent> </CardContent>
<CardFooter ClassName="justify-end"> <CardFooter ClassName="justify-end">
<WButtom OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)"> <WButton OnClick="DiagnoseAsync" disabled="@(!AccessResult.Succeeded)">
<StethoscopeIcon/> <StethoscopeIcon/>
Start diagnostics Start diagnostics
</WButtom> </WButton>
</CardFooter> </CardFooter>
</Card> </Card>
</div> </div>

View File

@@ -31,17 +31,13 @@
<HeartPulseIcon/> <HeartPulseIcon/>
Diagnose Diagnose
</TabsTrigger> </TabsTrigger>
<TabsTrigger Value="instance">
<ContainerIcon/>
Instance
</TabsTrigger>
</TabsList> </TabsList>
<TabsContent Value="settings"> <TabsContent Value="settings">
<Card ClassName="mt-5"> <Card ClassName="mt-5">
<CardContent>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<div class="col-span-1 grid gap-3">
<Label for="instance-name">Instance Name</Label>
<InputField id="instance-name" />
</div>
</div>
</CardContent>
<CardFooter ClassName="justify-end"> <CardFooter ClassName="justify-end">
<Button> <Button>
<SaveIcon /> <SaveIcon />
@@ -65,6 +61,9 @@
<Moonlight.Frontend.UI.Admin.Views.Sys.Themes.Index /> <Moonlight.Frontend.UI.Admin.Views.Sys.Themes.Index />
</TabsContent> </TabsContent>
} }
<TabsContent Value="instance">
<Instance />
</TabsContent>
</Tabs> </Tabs>
@code @code

View File

@@ -0,0 +1,201 @@
@using System.Text.RegularExpressions
@using LucideBlazor
@using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Emptys
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Selects
@using ShadcnBlazor.Switches
@inject HttpClient HttpClient
@inject DialogService DialogService
@inject AlertDialogService AlertDialogService
<div class="mt-5">
<LazyLoader Load="LoadAsync">
@if (StatusDto.IsEnabled)
{
if (StatusDto.IsReachable)
{
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
<Card ClassName="col-span-1">
<CardHeader>
<CardTitle>Version</CardTitle>
</CardHeader>
<CardContent>
<FieldGroup>
<FieldSet>
<Field>
<FieldLabel>Version / Branch</FieldLabel>
<FieldContent>
<Select DefaultValue="@SelectedVersion" @bind-Value="SelectedVersion">
<SelectTrigger ClassName="w-64">
<SelectValue/>
</SelectTrigger>
<SelectContent ClassName="w-64">
<SelectItem Value="v2.1">v2.1</SelectItem>
<SelectItem Value="v2.1.1">v2.1.1</SelectItem>
<SelectItem Value="feat/ContainerHelper">feat/ContainerHelper
</SelectItem>
</SelectContent>
</Select>
</FieldContent>
</Field>
<Field>
<FieldLabel>Bypass Build Cache</FieldLabel>
<FieldContent>
<Switch @bind-Value="NoBuildCache" />
</FieldContent>
</Field>
</FieldSet>
<Field Orientation="FieldOrientation.Horizontal">
<Button @onclick="AskApplyAsync">Apply</Button>
</Field>
</FieldGroup>
</CardContent>
</Card>
<Card ClassName="col-span-1">
<CardHeader>
<CardTitle>Plugins</CardTitle>
</CardHeader>
<CardContent>
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<SearchIcon/>
</EmptyMedia>
<EmptyTitle>No Plugins found</EmptyTitle>
<EmptyDescription>
No plugins found in instance configuration
</EmptyDescription>
</EmptyHeader>
</Empty>
</CardContent>
</Card>
</div>
}
else
{
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<CircleAlertIcon ClassName="text-red-500"/>
</EmptyMedia>
<EmptyTitle>Container Helper unreachable</EmptyTitle>
<EmptyDescription>
The container helper is unreachable. No management actions are available
</EmptyDescription>
</EmptyHeader>
</Empty>
}
}
else
{
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<ToggleLeftIcon/>
</EmptyMedia>
<EmptyTitle>Container Helper is disabled</EmptyTitle>
<EmptyDescription>
The container helper is disabled on this instance.
This might be due to running a multiple container moonlight setup
</EmptyDescription>
</EmptyHeader>
</Empty>
}
</LazyLoader>
</div>
@code
{
private ContainerHelperStatusDto StatusDto;
private string SelectedVersion = "v2.1";
private bool NoBuildCache;
private async Task LoadAsync(LazyLoader _)
{
StatusDto = (await HttpClient.GetFromJsonAsync<ContainerHelperStatusDto>("api/admin/ch/status"))!;
}
private async Task ApplyAsync()
{
await DialogService.LaunchAsync<UpdateInstanceModal>(
parameters =>
{
parameters[nameof(UpdateInstanceModal.Version)] = SelectedVersion;
parameters[nameof(UpdateInstanceModal.NoBuildCache)] = NoBuildCache;
},
onConfigure: model =>
{
model.ShowCloseButton = false;
model.ClassName = "sm:max-w-4xl!";
}
);
}
private async Task AskApplyAsync()
{
if (string.IsNullOrWhiteSpace(SelectedVersion))
return;
var shouldContinue = await ConfirmRiskyVersionAsync(
"Moonlight Rebuild",
"If you continue the moonlight instance will become unavailable during the rebuild process. This will impact users on this instance"
);
if (!shouldContinue)
return;
if (!Regex.IsMatch(SelectedVersion, @"^v\d+(\.\d+)*b?$"))
{
shouldContinue = await ConfirmRiskyVersionAsync(
"Development Version",
"You are about to install development a version. This can break your instance. Continue at your own risk"
);
}
else
{
if (SelectedVersion.EndsWith('b'))
{
shouldContinue = await ConfirmRiskyVersionAsync(
"Beta / Pre-Release Version",
"You are about to install a version marked as pre-release / beta. This can break your instance. Continue at your own risk"
);
}
else
shouldContinue = true;
}
if (!shouldContinue)
return;
await ApplyAsync();
}
private async Task<bool> ConfirmRiskyVersionAsync(string title, string message)
{
var tcs = new TaskCompletionSource();
var confirmed = false;
await AlertDialogService.ConfirmDangerAsync(
title,
message,
() =>
{
confirmed = true;
tcs.SetResult();
return Task.CompletedTask;
}
);
await tcs.Task;
return confirmed;
}
}

View File

@@ -3,14 +3,15 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Moonlight.Shared @using Moonlight.Shared
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Services @using Moonlight.Frontend.Services
@using Moonlight.Shared.Http.Requests.Themes @using Moonlight.Shared.Http.Requests.Admin.Themes
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.Labels
@using ShadcnBlazor.Cards @using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.Editors @using ShadcnBlazor.Extras.Editors
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Switches @using ShadcnBlazor.Switches
@@ -21,86 +22,89 @@
@inject ToastService ToastService @inject ToastService ToastService
@inject FrontendService FrontendService @inject FrontendService FrontendService
<div class="flex flex-row justify-between"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="editFormContext">
<div class="flex flex-col"> <div class="flex flex-row justify-between">
<h1 class="text-xl font-semibold">Create theme</h1> <div class="flex flex-col">
<div class="text-muted-foreground"> <h1 class="text-xl font-semibold">Create theme</h1>
Create a new theme <div class="text-muted-foreground">
Create a new theme
</div>
</div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/system?tab=themes" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
</Slot>
</Button>
<SubmitButton>
<CheckIcon/>
Continue
</SubmitButton>
</div> </div>
</div> </div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/system?tab=themes" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
</Slot>
</Button>
<Button @onclick="SubmitAsync">
<CheckIcon/>
Continue
</Button>
</div>
</div>
<div class="mt-8"> <div class="mt-8">
<Card> <Card>
<CardContent> <CardContent>
<FormHandler @ref="Form" OnValidSubmit="OnSubmitAsync" Model="Request"> <FieldGroup>
<div class="flex flex-col gap-6"> <FormValidationSummary/>
<DataAnnotationsValidator/>
<FormValidationSummary />
<FieldSet ClassName="grid grid-cols-1 lg:grid-cols-2">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5"> <Field>
<div class="col-span-1 grid gap-2"> <FieldLabel for="themeName">Name</FieldLabel>
<Label for="themeName">Name</Label> <TextInputField
<InputField
@bind-Value="Request.Name" @bind-Value="Request.Name"
id="themeName" id="themeName"
placeholder="My cool theme"/> placeholder="My cool theme"/>
</div> </Field>
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeVersion">Version</Label> <FieldLabel for="themeVersion">Version</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Version" @bind-Value="Request.Version"
id="themeVersion" id="themeVersion"
Type="text" Type="text"
placeholder="1.0.0"/> placeholder="1.0.0"/>
</div> </Field>
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeAuthor">Author</Label> <FieldLabel for="themeAuthor">Author</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Author" @bind-Value="Request.Author"
id="themeAuthor" id="themeAuthor"
Type="text" Type="text"
placeholder="Your name"/> placeholder="Your name"/>
</div> </Field>
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeAuthor">Is Enabled</Label> <FieldLabel for="themeAuthor">Is Enabled</FieldLabel>
<Switch @bind-Value="Request.IsEnabled" /> <FieldContent>
</div> <Switch @bind-Value="Request.IsEnabled"/>
</div> </FieldContent>
</Field>
<style> </FieldSet>
.cm-editor { <Field>
max-height: 400px; <style>
min-height: 400px; .cm-editor {
} max-height: 400px;
</style> min-height: 400px;
}
<div class="grid gap-2"> </style>
<Label for="themeAuthor">CSS Content</Label>
<Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/> <FieldLabel for="themeAuthor">CSS Content</FieldLabel>
</div> <FieldContent>
</div> <Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/>
</FormHandler> </FieldContent>
</CardContent> </Field>
</Card> </FieldGroup>
</div> </CardContent>
</Card>
</div>
</EnhancedEditForm>
@code @code
{ {
@@ -109,22 +113,23 @@
CssContent = "/* Define your css here */" CssContent = "/* Define your css here */"
}; };
private FormHandler Form;
private Editor Editor; private Editor Editor;
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.CssContent = await Editor.GetValueAsync(); Request.CssContent = await Editor.GetValueAsync();
await Form.SubmitAsync();
}
private async Task OnSubmitAsync() var response = await HttpClient.PostAsJsonAsync(
{
await HttpClient.PostAsJsonAsync(
"/api/admin/themes", "/api/admin/themes",
Request, Request,
Constants.SerializerOptions Constants.SerializerOptions
); );
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync( await ToastService.SuccessAsync(
"Theme creation", "Theme creation",
@@ -134,5 +139,7 @@
await FrontendService.ReloadAsync(); await FrontendService.ReloadAsync();
Navigation.NavigateTo("/admin/system?tab=themes"); Navigation.NavigateTo("/admin/system?tab=themes");
return true;
} }
} }

View File

@@ -4,7 +4,7 @@
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Themes @using Moonlight.Shared.Http.Responses.Admin.Themes
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dropdowns @using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Extras.AlertDialogs @using ShadcnBlazor.Extras.AlertDialogs

View File

@@ -3,17 +3,18 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Moonlight.Shared @using Moonlight.Shared
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.Services @using Moonlight.Frontend.Services
@using Moonlight.Shared.Http.Requests.Themes @using Moonlight.Shared.Http.Requests.Admin.Themes
@using Moonlight.Shared.Http.Responses.Themes @using Moonlight.Shared.Http.Responses.Admin.Themes
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.Labels
@using ShadcnBlazor.Cards @using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.Common @using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Editors @using ShadcnBlazor.Extras.Editors
@using ShadcnBlazor.Extras.FormHandlers @using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs @using ShadcnBlazor.Inputs
@using ShadcnBlazor.Switches @using ShadcnBlazor.Switches
@@ -24,99 +25,102 @@
@inject ToastService ToastService @inject ToastService ToastService
@inject FrontendService FrontendService @inject FrontendService FrontendService
<div class="flex flex-row justify-between"> <LazyLoader Load="LoadAsync">
<div class="flex flex-col"> <EnhancedEditForm Model="Request" OnValidSubmit="OnSubmitAsync" Context="editFormContext">
<h1 class="text-xl font-semibold">Update theme</h1> <div class="flex flex-row justify-between">
<div class="text-muted-foreground"> <div class="flex flex-col">
Update the theme <h1 class="text-xl font-semibold">Update theme</h1>
<div class="text-muted-foreground">
Update the theme
</div>
</div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/system?tab=themes" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
</Slot>
</Button>
<SubmitButton>
<CheckIcon/>
Continue
</SubmitButton>
</div>
</div> </div>
</div>
<div class="flex flex-row gap-x-1.5">
<Button Variant="ButtonVariant.Secondary">
<Slot>
<a href="/admin/system?tab=themes" @attributes="context">
<ChevronLeftIcon/>
Back
</a>
</Slot>
</Button>
<Button @onclick="SubmitAsync">
<CheckIcon/>
Continue
</Button>
</div>
</div>
<div class="mt-8">
<Card>
<CardContent>
<LazyLoader Load="LoadAsync">
<FormHandler @ref="Form" OnValidSubmit="OnSubmitAsync" Model="Request">
<div class="flex flex-col gap-6">
<div class="mt-8">
<Card>
<CardContent>
<FieldGroup>
<FormValidationSummary/> <FormValidationSummary/>
<DataAnnotationsValidator/>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5"> <FieldSet ClassName="grid grid-cols-1 lg:grid-cols-2">
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeName">Name</Label> <FieldLabel for="themeName">Name</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Name" @bind-Value="Request.Name"
id="themeName" id="themeName"
placeholder="My cool theme"/> placeholder="My cool theme"/>
</div> </Field>
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeVersion">Version</Label> <FieldLabel for="themeVersion">Version</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Version" @bind-Value="Request.Version"
id="themeVersion" id="themeVersion"
Type="text" Type="text"
placeholder="1.0.0"/> placeholder="1.0.0"/>
</div> </Field>
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeAuthor">Author</Label> <FieldLabel for="themeAuthor">Author</FieldLabel>
<InputField <TextInputField
@bind-Value="Request.Author" @bind-Value="Request.Author"
id="themeAuthor" id="themeAuthor"
Type="text" Type="text"
placeholder="Your name"/> placeholder="Your name"/>
</div> </Field>
<div class="col-span-1 grid gap-2"> <Field>
<Label for="themeAuthor">Is Enabled</Label> <FieldLabel for="themeAuthor">Is Enabled</FieldLabel>
<Switch @bind-Value="Request.IsEnabled" /> <FieldContent>
</div> <Switch @bind-Value="Request.IsEnabled"/>
</div> </FieldContent>
</Field>
</FieldSet>
<Field>
<style>
.cm-editor {
max-height: 400px;
min-height: 400px;
}
</style>
<style> <FieldLabel for="themeAuthor">CSS Content</FieldLabel>
.cm-editor { <FieldContent>
max-height: 400px; <Editor @ref="Editor" Language="EditorLanguage.Css"
min-height: 400px; InitialValue="@Request.CssContent"/>
} </FieldContent>
</style> </Field>
</FieldGroup>
<div class="grid gap-2"> </CardContent>
<Label for="themeAuthor">CSS Content</Label> </Card>
<Editor @ref="Editor" Language="EditorLanguage.Css" InitialValue="@Request.CssContent"/> </div>
</div> </EnhancedEditForm>
</div> </LazyLoader>
</FormHandler>
</LazyLoader>
</CardContent>
</Card>
</div>
@code @code
{ {
[Parameter] public int Id { get; set; } [Parameter] public int Id { get; set; }
private UpdateThemeDto Request; private UpdateThemeDto Request;
private ThemeDto Theme; private ThemeDto Theme;
private FormHandler Form;
private Editor Editor; private Editor Editor;
private async Task LoadAsync(LazyLoader _) private async Task LoadAsync(LazyLoader _)
{ {
var theme = await HttpClient.GetFromJsonAsync<ThemeDto>($"api/admin/themes/{Id}"); var theme = await HttpClient.GetFromJsonAsync<ThemeDto>($"api/admin/themes/{Id}");
@@ -125,19 +129,21 @@
Request = ThemeMapper.ToUpdate(Theme); Request = ThemeMapper.ToUpdate(Theme);
} }
private async Task SubmitAsync() private async Task<bool> OnSubmitAsync(EditContext editContext, ValidationMessageStore validationMessageStore)
{ {
Request.CssContent = await Editor.GetValueAsync(); Request.CssContent = await Editor.GetValueAsync();
await Form.SubmitAsync();
}
private async Task OnSubmitAsync() var response = await HttpClient.PatchAsJsonAsync(
{
await HttpClient.PatchAsJsonAsync(
$"/api/admin/themes/{Theme.Id}", $"/api/admin/themes/{Theme.Id}",
Request, Request,
Constants.SerializerOptions Constants.SerializerOptions
); );
if (!response.IsSuccessStatusCode)
{
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
await ToastService.SuccessAsync( await ToastService.SuccessAsync(
"Theme update", "Theme update",
@@ -147,5 +153,7 @@
await FrontendService.ReloadAsync(); await FrontendService.ReloadAsync();
Navigation.NavigateTo("/admin/system?tab=themes"); Navigation.NavigateTo("/admin/system?tab=themes");
return true;
} }
} }

View File

@@ -4,7 +4,6 @@
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Requests.Roles
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@@ -134,15 +133,8 @@
{ {
await DialogService.LaunchAsync<CreateRoleDialog>(parameters => await DialogService.LaunchAsync<CreateRoleDialog>(parameters =>
{ {
parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task (CreateRoleDto request) => parameters[nameof(CreateRoleDialog.OnSubmit)] = async Task () =>
{ {
await HttpClient.PostAsJsonAsync(
"api/admin/roles",
request,
Constants.SerializerOptions
);
await ToastService.SuccessAsync("Role creation", $"Role {request.Name} has been successfully created");
await Grid.RefreshAsync(); await Grid.RefreshAsync();
}; };
}); });
@@ -153,15 +145,8 @@
await DialogService.LaunchAsync<UpdateRoleDialog>(parameters => await DialogService.LaunchAsync<UpdateRoleDialog>(parameters =>
{ {
parameters[nameof(UpdateRoleDialog.Role)] = role; parameters[nameof(UpdateRoleDialog.Role)] = role;
parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task (UpdateRoleDto request) => parameters[nameof(UpdateRoleDialog.OnSubmit)] = async Task () =>
{ {
await HttpClient.PatchAsJsonAsync(
$"api/admin/roles/{role.Id}",
request,
Constants.SerializerOptions
);
await ToastService.SuccessAsync("Role update", $"Role {request.Name} has been successfully updated");
await Grid.RefreshAsync(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -10,9 +10,8 @@
@using ShadcnBlazor.Extras.Toasts @using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Tabels @using ShadcnBlazor.Tabels
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Requests.Users
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Users @using Moonlight.Shared.Http.Responses.Admin.Users
@using ShadcnBlazor.Extras.Dialogs @using ShadcnBlazor.Extras.Dialogs
@inject HttpClient HttpClient @inject HttpClient HttpClient
@@ -124,19 +123,8 @@
{ {
await DialogService.LaunchAsync<CreateUserDialog>(parameters => await DialogService.LaunchAsync<CreateUserDialog>(parameters =>
{ {
parameters[nameof(CreateUserDialog.OnSubmit)] = async (CreateUserDto dto) => parameters[nameof(CreateUserDialog.OnCompleted)] = async () =>
{ {
await HttpClient.PostAsJsonAsync(
"/api/admin/users",
dto,
Constants.SerializerOptions
);
await ToastService.SuccessAsync(
"User creation",
$"Successfully created user {dto.Username}"
);
await Grid.RefreshAsync(); await Grid.RefreshAsync();
}; };
}); });
@@ -147,18 +135,8 @@
await DialogService.LaunchAsync<UpdateUserDialog>(parameters => await DialogService.LaunchAsync<UpdateUserDialog>(parameters =>
{ {
parameters[nameof(UpdateUserDialog.User)] = user; parameters[nameof(UpdateUserDialog.User)] = user;
parameters[nameof(CreateUserDialog.OnSubmit)] = async (UpdateUserDto dto) => parameters[nameof(UpdateUserDialog.OnCompleted)] = async () =>
{ {
await HttpClient.PatchAsJsonAsync(
$"/api/admin/users/{user.Id}",
dto
);
await ToastService.SuccessAsync(
"User update",
$"Successfully updated user {dto.Username}"
);
await Grid.RefreshAsync(); await Grid.RefreshAsync();
}; };
}); });

View File

@@ -1,7 +1,7 @@
@using ShadcnBlazor.Cards @using Moonlight.Shared.Http.Responses.Admin.Auth
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Spinners @using ShadcnBlazor.Spinners
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using Moonlight.Shared.Http.Responses.Auth
@inject HttpClient HttpClient @inject HttpClient HttpClient
@inject NavigationManager Navigation @inject NavigationManager Navigation

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
namespace Moonlight.Shared.Http.Events;
public struct RebuildEventDto
{
[JsonPropertyName("type")]
public RebuildEventType Type { get; set; }
[JsonPropertyName("data")]
public string Data { get; set; }
}
public enum RebuildEventType
{
Log = 0,
Failed = 1,
Succeeded = 2,
Step = 3
}

View File

@@ -1,13 +1,16 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.ApiKeys; namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys;
public class CreateApiKeyDto public class CreateApiKeyDto
{ {
[Required]
[MaxLength(30)] [MaxLength(30)]
public string Name { get; set; } public string Name { get; set; }
[MaxLength(300)] public string Description { get; set; } = ""; [MaxLength(300)] public string Description { get; set; } = "";
[Required]
public string[] Permissions { get; set; } public string[] Permissions { get; set; }
} }

View File

@@ -1,13 +1,15 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.ApiKeys; namespace Moonlight.Shared.Http.Requests.Admin.ApiKeys;
public class UpdateApiKeyDto public class UpdateApiKeyDto
{ {
[Required]
[MaxLength(30)] [MaxLength(30)]
public string Name { get; set; } public string Name { get; set; }
[MaxLength(300)] public string Description { get; set; } = ""; [MaxLength(300)] public string Description { get; set; } = "";
[Required]
public string[] Permissions { get; set; } public string[] Permissions { get; set; }
} }

View File

@@ -0,0 +1,15 @@
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
public class RequestRebuildDto
{
public bool NoBuildCache { get; set; }
public RequestRebuildDto()
{
}
public RequestRebuildDto(bool noBuildCache)
{
NoBuildCache = noBuildCache;
}
}

View File

@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
public class SetVersionDto
{
[Required]
[RegularExpression(@"^(?!\/|.*\/\/|.*\.\.|.*\/$)[A-Za-z0-9._/-]+$", ErrorMessage = "Invalid version format")]
public string Version { get; set; }
}

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Roles; namespace Moonlight.Shared.Http.Requests.Admin.Roles;
public class CreateRoleDto public class CreateRoleDto
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Roles; namespace Moonlight.Shared.Http.Requests.Admin.Roles;
public class UpdateRoleDto public class UpdateRoleDto
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Themes; namespace Moonlight.Shared.Http.Requests.Admin.Themes;
public class CreateThemeDto public class CreateThemeDto
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Themes; namespace Moonlight.Shared.Http.Requests.Admin.Themes;
public class UpdateThemeDto public class UpdateThemeDto
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Users; namespace Moonlight.Shared.Http.Requests.Admin.Users;
public class CreateUserDto public class CreateUserDto
{ {

View File

@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Users; namespace Moonlight.Shared.Http.Requests.Admin.Users;
public class UpdateUserDto public class UpdateUserDto
{ {

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.ApiKeys; namespace Moonlight.Shared.Http.Responses.Admin.ApiKeys;
public record ApiKeyDto(int Id, string Name, string Description, string[] Permissions, string Key, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt); public record ApiKeyDto(int Id, string Name, string Description, string[] Permissions, string Key, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin.Auth;
public record ClaimDto(string Type, string Value);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Auth; namespace Moonlight.Shared.Http.Responses.Admin.Auth;
public record SchemeDto(string Name, string DisplayName); public record SchemeDto(string Name, string DisplayName);

View File

@@ -0,0 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Admin;
public record ContainerHelperStatusDto(bool IsEnabled, bool IsReachable);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Frontend; namespace Moonlight.Shared.Http.Responses.Admin.Frontend;
public record FrontendConfigDto(string Name, string? ThemeCss); public record FrontendConfigDto(string Name, string? ThemeCss);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Themes; namespace Moonlight.Shared.Http.Responses.Admin.Themes;
public record ThemeDto(int Id, string Name, string Author, string Version, string CssContent, bool IsEnabled); public record ThemeDto(int Id, string Name, string Author, string Version, string CssContent, bool IsEnabled);

View File

@@ -1,3 +1,3 @@
namespace Moonlight.Shared.Http.Responses.Users; namespace Moonlight.Shared.Http.Responses.Admin.Users;
public record UserDto(int Id, string Username, string Email, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt); public record UserDto(int Id, string Username, string Email, DateTimeOffset CreatedAt, DateTimeOffset UpdatedAt);

View File

@@ -1,3 +0,0 @@
namespace Moonlight.Shared.Http.Responses.Auth;
public record ClaimDto(string Type, string Value);

View File

@@ -0,0 +1,10 @@
namespace Moonlight.Shared.Http.Responses;
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; }
}

View File

@@ -1,14 +1,17 @@
using System.Text.Json.Serialization; using System.Text.Json;
using Moonlight.Shared.Http.Requests.ApiKeys; using System.Text.Json.Serialization;
using Moonlight.Shared.Http.Requests.Roles; using Moonlight.Shared.Http.Events;
using Moonlight.Shared.Http.Requests.Themes; using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Requests.Users; using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Shared.Http.Responses.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Auth; using Moonlight.Shared.Http.Responses.Admin.Auth;
using Moonlight.Shared.Http.Responses.Themes; using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Http.Responses.Users; using Moonlight.Shared.Http.Responses.Admin.Users;
namespace Moonlight.Shared.Http; namespace Moonlight.Shared.Http;
@@ -44,8 +47,32 @@ namespace Moonlight.Shared.Http;
[JsonSerializable(typeof(PagedData<ThemeDto>))] [JsonSerializable(typeof(PagedData<ThemeDto>))]
[JsonSerializable(typeof(ThemeDto))] [JsonSerializable(typeof(ThemeDto))]
// Events
[JsonSerializable(typeof(RebuildEventDto))]
// Container Helper
[JsonSerializable(typeof(ContainerHelperStatusDto))]
[JsonSerializable(typeof(RequestRebuildDto))]
[JsonSerializable(typeof(SetVersionDto))]
//Misc //Misc
[JsonSerializable(typeof(VersionDto))] [JsonSerializable(typeof(VersionDto))]
[JsonSerializable(typeof(ProblemDetails))]
public partial class SerializationContext : JsonSerializerContext public partial class SerializationContext : JsonSerializerContext
{ {
private static JsonSerializerOptions? InternalTunedOptions;
public static JsonSerializerOptions TunedOptions
{
get
{
if (InternalTunedOptions != null)
return InternalTunedOptions;
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
return InternalTunedOptions;
}
}
} }

View File

@@ -36,4 +36,32 @@
# Logging # Logging
- "Logging__LogLevel__Default=Information" - "Logging__LogLevel__Default=Information"
- "Logging__LogLevel__Microsoft.AspNetCore=Warning" - "Logging__LogLevel__Microsoft.AspNetCore=Warning"
- "Logging__LogLevel__System.Net.Http.HttpClient=Warning"
- "Moonlight__ContainerHelper__IsEnabled=true"
- "Moonlight__ContainerHelper__Url=http://app:8080"
app:
image: git.battlestati.one/moonlight-panel/container_helper
group_add:
- "989"
environment:
# Logging
- "Logging__LogLevel__Default=Information"
- "Logging__LogLevel__Microsoft.AspNetCore=Warning"
# Compose
- "ContainerHelper__Compose__Directory=${PWD}"
- "ContainerHelper__Compose__Binary=docker-compose"
- "ContainerHelper__Service__Name=api"
# HTTP Proxy
- "HTTP_PROXY=${HTTP_PROXY}"
- "HTTPS_PROXY=${HTTPS_PROXY}"
volumes:
- "${PWD}:${PWD}"
- "/var/run/docker.sock:/var/run/docker.sock"