Added diagnose frontend and backend implementation
This commit is contained in:
29
Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs
Normal file
29
Moonlight.Api/Http/Controllers/Admin/DiagnoseController.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moonlight.Api.Mappers;
|
||||
using Moonlight.Api.Services;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
namespace Moonlight.Api.Http.Controllers.Admin;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/admin/system/diagnose")]
|
||||
public class DiagnoseController : Controller
|
||||
{
|
||||
private readonly DiagnoseService DiagnoseService;
|
||||
|
||||
public DiagnoseController(DiagnoseService diagnoseService)
|
||||
{
|
||||
DiagnoseService = diagnoseService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<DiagnoseResultResponse[]>> GetAsync()
|
||||
{
|
||||
var results = await DiagnoseService.DiagnoseAsync();
|
||||
|
||||
return results
|
||||
.OrderBy(x => x.Level)
|
||||
.MapToResult()
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
33
Moonlight.Api/Implementations/UpdateDiagnoseProvider.cs
Normal file
33
Moonlight.Api/Implementations/UpdateDiagnoseProvider.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Moonlight.Api.Interfaces;
|
||||
using Moonlight.Api.Models;
|
||||
using Moonlight.Api.Services;
|
||||
|
||||
namespace Moonlight.Api.Implementations;
|
||||
|
||||
public sealed class UpdateDiagnoseProvider : IDiagnoseProvider
|
||||
{
|
||||
private readonly ApplicationService ApplicationService;
|
||||
|
||||
public UpdateDiagnoseProvider(ApplicationService applicationService)
|
||||
{
|
||||
ApplicationService = applicationService;
|
||||
}
|
||||
|
||||
public Task<DiagnoseResult[]> DiagnoseAsync()
|
||||
{
|
||||
if (ApplicationService.IsUpToDate)
|
||||
return Task.FromResult<DiagnoseResult[]>([]);
|
||||
|
||||
return Task.FromResult<DiagnoseResult[]>([
|
||||
new DiagnoseResult(
|
||||
DiagnoseLevel.Warning,
|
||||
"Instance is not up-to-date",
|
||||
["Moonlight", "Update Check"],
|
||||
"Update your moonlight instance to receive bug fixes, new features and security patches. Update button can be found in the overview",
|
||||
null,
|
||||
"/admin",
|
||||
null
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
8
Moonlight.Api/Interfaces/IDiagnoseProvider.cs
Normal file
8
Moonlight.Api/Interfaces/IDiagnoseProvider.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Moonlight.Api.Models;
|
||||
|
||||
namespace Moonlight.Api.Interfaces;
|
||||
|
||||
public interface IDiagnoseProvider
|
||||
{
|
||||
public Task<DiagnoseResult[]> DiagnoseAsync();
|
||||
}
|
||||
14
Moonlight.Api/Mappers/DiagnoseResultMapper.cs
Normal file
14
Moonlight.Api/Mappers/DiagnoseResultMapper.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Moonlight.Api.Models;
|
||||
using Moonlight.Shared.Http.Responses.Admin;
|
||||
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 DiagnoseResultMapper
|
||||
{
|
||||
public static partial IEnumerable<DiagnoseResultResponse> MapToResult(this IEnumerable<DiagnoseResult> results);
|
||||
}
|
||||
10
Moonlight.Api/Models/DiagnoseResult.cs
Normal file
10
Moonlight.Api/Models/DiagnoseResult.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.Api.Models;
|
||||
|
||||
public record DiagnoseResult(DiagnoseLevel Level, string Title, string[] Tags, string? Message, string? StackStrace, string? SolutionUrl, string? ReportUrl);
|
||||
|
||||
public enum DiagnoseLevel
|
||||
{
|
||||
Error = 0,
|
||||
Warning = 1,
|
||||
Healthy = 2
|
||||
}
|
||||
38
Moonlight.Api/Services/DiagnoseService.cs
Normal file
38
Moonlight.Api/Services/DiagnoseService.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moonlight.Api.Interfaces;
|
||||
using Moonlight.Api.Models;
|
||||
|
||||
namespace Moonlight.Api.Services;
|
||||
|
||||
public class DiagnoseService
|
||||
{
|
||||
private readonly IEnumerable<IDiagnoseProvider> Providers;
|
||||
private readonly ILogger<DiagnoseService> Logger;
|
||||
|
||||
public DiagnoseService(IEnumerable<IDiagnoseProvider> providers, ILogger<DiagnoseService> logger)
|
||||
{
|
||||
Providers = providers;
|
||||
Logger = logger;
|
||||
}
|
||||
|
||||
public async Task<DiagnoseResult[]> DiagnoseAsync()
|
||||
{
|
||||
var results = new List<DiagnoseResult>();
|
||||
|
||||
foreach (var provider in Providers)
|
||||
{
|
||||
try
|
||||
{
|
||||
results.AddRange(
|
||||
await provider.DiagnoseAsync()
|
||||
);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError(e, "An unhandled error occured while processing provider");
|
||||
}
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
using Moonlight.Shared.Http;
|
||||
using Moonlight.Api.Helpers;
|
||||
using Moonlight.Api.Implementations;
|
||||
using Moonlight.Api.Interfaces;
|
||||
using Moonlight.Api.Services;
|
||||
|
||||
namespace Moonlight.Api.Startup;
|
||||
@@ -23,6 +25,10 @@ public partial class Startup
|
||||
|
||||
builder.Services.AddSingleton<ApplicationService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||
|
||||
builder.Services.AddSingleton<DiagnoseService>();
|
||||
|
||||
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||
}
|
||||
|
||||
private static void UseBase(WebApplication application)
|
||||
|
||||
228
Moonlight.Frontend/UI/Admin/Views/Settings/Diagnose.razor
Normal file
228
Moonlight.Frontend/UI/Admin/Views/Settings/Diagnose.razor
Normal file
@@ -0,0 +1,228 @@
|
||||
@using LucideBlazor
|
||||
@using Moonlight.Shared.Http.Responses.Admin
|
||||
@using ShadcnBlazor.Accordions
|
||||
@using ShadcnBlazor.Alerts
|
||||
@using ShadcnBlazor.Buttons
|
||||
@using ShadcnBlazor.Cards
|
||||
@using ShadcnBlazor.Emptys
|
||||
@using ShadcnBlazor.Extras.Common
|
||||
@using ShadcnBlazor.Spinners
|
||||
|
||||
@inject HttpClient HttpClient
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-2 gap-5 mt-5">
|
||||
<div class="col-span-1">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Automatic diagnosis</CardTitle>
|
||||
<CardDescription>
|
||||
Use a diagnostic report to share configuration details and errors with Moonlight developers, with
|
||||
sensitive data automatically censored.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent ClassName="flex flex-col gap-y-5">
|
||||
<Alert
|
||||
ClassName="w-full flex flex-row items-center gap-3 border-yellow-500/80 bg-yellow-500/5 text-yellow-500">
|
||||
<div class="flex shrink-0 items-center">
|
||||
<TriangleAlertIcon ClassName="size-6 text-yellow-500/60"/>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center justify-between gap-4">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<AlertTitle>Notice</AlertTitle>
|
||||
<AlertDescription ClassName="text-yellow-500/80">
|
||||
Only share these reports with the moonlight developers or the corresponding plugin
|
||||
developers.
|
||||
Even though we do our best to censor sensitive data it may still contain information you
|
||||
dont want a random person on the internet to know
|
||||
</AlertDescription>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
<CardFooter ClassName="justify-end">
|
||||
<WButtom OnClick="DiagnoseAsync">
|
||||
<StethoscopeIcon/>
|
||||
Start diagnostics
|
||||
</WButtom>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
<div class="col-span-1">
|
||||
<Card>
|
||||
<CardContent ClassName="flex justify-center items-center">
|
||||
@if (IsLoading)
|
||||
{
|
||||
<Spinner ClassName="size-10"/>
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HasDiagnosed)
|
||||
{
|
||||
if (Entries.Length == 0)
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<SearchIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No results available</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Diagnosis didnt return any results
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
else
|
||||
{
|
||||
<Accordion
|
||||
ClassName="w-full"
|
||||
Type="AccordionType.Single">
|
||||
|
||||
@for (var i = 0; i < Entries.Length; i++)
|
||||
{
|
||||
var entry = Entries[i];
|
||||
|
||||
var textColor = entry.Level switch
|
||||
{
|
||||
DiagnoseLevel.Error => "text-destructive",
|
||||
DiagnoseLevel.Warning => "text-yellow-300",
|
||||
DiagnoseLevel.Healthy => "text-green-500"
|
||||
};
|
||||
|
||||
<AccordionItem
|
||||
ClassName="overflow-hidden border bg-background px-4 first:rounded-t-lg last:rounded-b-lg last:border-b"
|
||||
Value="@($"diagnoseEntry{i}")">
|
||||
|
||||
<AccordionTrigger className="hover:no-underline">
|
||||
<div class="flex items-center gap-3">
|
||||
|
||||
@switch (entry.Level)
|
||||
{
|
||||
case DiagnoseLevel.Error:
|
||||
<CircleXIcon ClassName="@textColor"/>
|
||||
break;
|
||||
|
||||
case DiagnoseLevel.Warning:
|
||||
<TriangleAlertIcon ClassName="@textColor"/>
|
||||
break;
|
||||
|
||||
case DiagnoseLevel.Healthy:
|
||||
<CircleCheckIcon ClassName="@textColor"/>
|
||||
break;
|
||||
}
|
||||
|
||||
<div class="flex flex-col items-start text-left">
|
||||
<span class="@textColor">
|
||||
@entry.Title
|
||||
</span>
|
||||
<span class="text-sm text-muted-foreground">
|
||||
@(string.Join(" / ", entry.Tags))
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent ClassName="ps-7">
|
||||
<div class="text-muted-foreground flex flex-col gap-y-3">
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(entry.StackStrace))
|
||||
{
|
||||
<div
|
||||
class="rounded-xl p-2.5 bg-black max-h-36 overflow-auto scrollbar-thin">
|
||||
@entry.StackStrace
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(entry.Message))
|
||||
{
|
||||
<p>
|
||||
@entry.Message
|
||||
</p>
|
||||
}
|
||||
|
||||
@if (
|
||||
!string.IsNullOrWhiteSpace(entry.ReportUrl) ||
|
||||
!string.IsNullOrWhiteSpace(entry.StackStrace) ||
|
||||
!string.IsNullOrWhiteSpace(entry.SolutionUrl)
|
||||
)
|
||||
{
|
||||
<div class="flex justify-end gap-x-1">
|
||||
@if (!string.IsNullOrWhiteSpace(entry.StackStrace))
|
||||
{
|
||||
<Button Variant="ButtonVariant.Outline">
|
||||
<CopyIcon/>
|
||||
Copy
|
||||
</Button>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(entry.SolutionUrl))
|
||||
{
|
||||
<Button Variant="ButtonVariant.Outline">
|
||||
<Slot>
|
||||
<a href="@entry.SolutionUrl" @attributes="context">
|
||||
<WrenchIcon/>
|
||||
Show suggested solution
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(entry.ReportUrl))
|
||||
{
|
||||
<Button Variant="ButtonVariant.Outline">
|
||||
<Slot>
|
||||
<a href="@entry.ReportUrl" @attributes="context">
|
||||
<GitBranchIcon/>
|
||||
Report on Github
|
||||
</a>
|
||||
</Slot>
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
}
|
||||
</Accordion>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia Variant="EmptyMediaVariant.Icon">
|
||||
<CircleQuestionMarkIcon/>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No results available yet</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Press the start button to start the automatic diagnosis
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
}
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool IsLoading = false;
|
||||
private bool HasDiagnosed = false;
|
||||
private DiagnoseResultResponse[] Entries;
|
||||
|
||||
private async Task DiagnoseAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
HasDiagnosed = false;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
var results = await HttpClient.GetFromJsonAsync<DiagnoseResultResponse[]>("api/admin/system/diagnose");
|
||||
Entries = results ?? [];
|
||||
|
||||
IsLoading = false;
|
||||
HasDiagnosed = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -43,4 +43,7 @@
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent Value="diagnose">
|
||||
<Diagnose />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Moonlight.Shared.Http.Responses.Admin;
|
||||
|
||||
public record DiagnoseResultResponse(DiagnoseLevel Level, string Title, string[] Tags, string? Message, string? StackStrace, string? SolutionUrl, string? ReportUrl);
|
||||
|
||||
public enum DiagnoseLevel
|
||||
{
|
||||
Error = 0,
|
||||
Warning = 1,
|
||||
Healthy = 2
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace Moonlight.Shared.Http;
|
||||
[JsonSerializable(typeof(UpdateUserRequest))]
|
||||
[JsonSerializable(typeof(ClaimResponse[]))]
|
||||
[JsonSerializable(typeof(SchemeResponse[]))]
|
||||
[JsonSerializable(typeof(DiagnoseResultResponse[]))]
|
||||
[JsonSerializable(typeof(UserResponse))]
|
||||
[JsonSerializable(typeof(SystemInfoResponse))]
|
||||
[JsonSerializable(typeof(PagedData<UserResponse>))]
|
||||
|
||||
Reference in New Issue
Block a user