Implemented container helper status checked. Started implementing container helper ui. Improved update modal

This commit is contained in:
2026-01-25 22:51:51 +01:00
parent e2f344ab4e
commit 4e96905fb2
7 changed files with 152 additions and 37 deletions

View File

@@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration;
using Moonlight.Api.Services; using Moonlight.Api.Services;
using Moonlight.Shared.Http.Responses.Admin;
namespace Moonlight.Api.Http.Controllers.Admin; namespace Moonlight.Api.Http.Controllers.Admin;
@@ -9,10 +12,23 @@ namespace Moonlight.Api.Http.Controllers.Admin;
public class ChController : Controller public class ChController : Controller
{ {
private readonly ContainerHelperService ContainerHelperService; private readonly ContainerHelperService ContainerHelperService;
private readonly IOptions<ContainerHelperOptions> Options;
public ChController(ContainerHelperService containerHelperService) public ChController(ContainerHelperService containerHelperService, IOptions<ContainerHelperOptions> options)
{ {
ContainerHelperService = containerHelperService; 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")] [HttpPost("rebuild")]

View File

@@ -13,6 +13,23 @@ public class ContainerHelperService
HttpClientFactory = 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<RebuildEvent> RebuildAsync() public async IAsyncEnumerable<RebuildEvent> RebuildAsync()
{ {
var options = new JsonSerializerOptions() var options = new JsonSerializerOptions()
@@ -29,6 +46,7 @@ public class ContainerHelperService
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
{ {
var responseText = await response.Content.ReadAsStringAsync(); var responseText = await response.Content.ReadAsStringAsync();
yield return new RebuildEvent() yield return new RebuildEvent()
{ {
Type = RebuildEventType.Failed, Type = RebuildEventType.Failed,
@@ -56,6 +74,11 @@ public class ContainerHelperService
var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, options); var deserializedData = JsonSerializer.Deserialize<RebuildEvent>(data, options);
yield return deserializedData; 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); } while (true);
yield return new RebuildEvent() yield return new RebuildEvent()

View File

@@ -2,14 +2,12 @@
@using System.Text.Json @using System.Text.Json
@using LucideBlazor @using LucideBlazor
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Events @using Moonlight.Shared.Http.Events
@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 @inject HttpClient HttpClient
<DialogHeader> <DialogHeader>
@@ -62,46 +60,57 @@
</div> </div>
</div> </div>
<DialogFooter> @if (CurrentStep == Steps.Length)
<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; 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
"Starting rebuild task", "Starting rebuild task", // 2
"Building docker image", "Building docker image", // 3
"Redeploying container instance", "Redeploying container instance", // 4
"Waiting for container instance to start up", "Waiting for container instance to start up", // 5
"Update complete" "Update complete" // 6
]; ];
private List<string?> LogLines = new(); 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(2000); await Task.Delay(2000);
// Starting rebuild task
CurrentStep = 2; CurrentStep = 2;
Progress = 30; Progress = 30;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@@ -140,20 +149,22 @@
switch (deserializedData.Data) switch (deserializedData.Data)
{ {
case "BuildImage": case "BuildImage":
// Building docker image
CurrentStep = 3; CurrentStep = 3;
Progress = 40; Progress = 40;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
break; break;
case "ServiceDown": case "ServiceDown":
// Redeploying container instance
CurrentStep = 4; CurrentStep = 4;
Progress = 60; Progress = 60;
await InvokeAsync(StateHasChanged);
break;
case "ServiceUp":
CurrentStep = 4;
Progress = 80;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
break; break;
} }
@@ -163,17 +174,21 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
catch (Exception e) catch (Exception)
{ {
// TODO: Log
break; break;
} }
} while (true); } while (true);
// Waiting for container instance to start up
CurrentStep = 5; CurrentStep = 5;
Progress = 90; Progress = 90;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
// Wait some time for instance to shut down
await Task.Delay(TimeSpan.FromSeconds(5));
// Ping instance until its reachable again // Ping instance until its reachable again
while (true) while (true)
{ {
@@ -186,21 +201,13 @@
{ {
// Ignored // Ignored
} }
await Task.Delay(3000); await Task.Delay(3000);
} }
CurrentStep = 6; // Update complete
CurrentStep = 7;
Progress = 100; Progress = 100;
await InvokeAsync(StateHasChanged); 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();
} }
} }

View File

@@ -31,6 +31,10 @@
<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">
@@ -65,6 +69,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,56 @@
@using LucideBlazor
@using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Emptys
@using ShadcnBlazor.Extras.Common
@inject HttpClient HttpClient
<div class="mt-5">
<LazyLoader Load="LoadAsync">
@if (StatusDto.IsEnabled)
{
if (StatusDto.IsReachable)
{
}
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 async Task LoadAsync(LazyLoader _)
{
StatusDto = (await HttpClient.GetFromJsonAsync<ContainerHelperStatusDto>("api/admin/ch/status"))!;
}
}

View File

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

View File

@@ -47,6 +47,9 @@ namespace Moonlight.Shared.Http;
// Events // Events
[JsonSerializable(typeof(RebuildEvent))] [JsonSerializable(typeof(RebuildEvent))]
// Container Helper
[JsonSerializable(typeof(ContainerHelperStatusDto))]
public partial class SerializationContext : JsonSerializerContext public partial class SerializationContext : JsonSerializerContext
{ {
} }