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 Microsoft.Extensions.Logging.Console;
|
||||||
using Moonlight.Shared.Http;
|
using Moonlight.Shared.Http;
|
||||||
using Moonlight.Api.Helpers;
|
using Moonlight.Api.Helpers;
|
||||||
|
using Moonlight.Api.Implementations;
|
||||||
|
using Moonlight.Api.Interfaces;
|
||||||
using Moonlight.Api.Services;
|
using Moonlight.Api.Services;
|
||||||
|
|
||||||
namespace Moonlight.Api.Startup;
|
namespace Moonlight.Api.Startup;
|
||||||
@@ -23,6 +25,10 @@ public partial class Startup
|
|||||||
|
|
||||||
builder.Services.AddSingleton<ApplicationService>();
|
builder.Services.AddSingleton<ApplicationService>();
|
||||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<ApplicationService>());
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<DiagnoseService>();
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IDiagnoseProvider, UpdateDiagnoseProvider>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UseBase(WebApplication application)
|
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>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent Value="diagnose">
|
||||||
|
<Diagnose />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</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(UpdateUserRequest))]
|
||||||
[JsonSerializable(typeof(ClaimResponse[]))]
|
[JsonSerializable(typeof(ClaimResponse[]))]
|
||||||
[JsonSerializable(typeof(SchemeResponse[]))]
|
[JsonSerializable(typeof(SchemeResponse[]))]
|
||||||
|
[JsonSerializable(typeof(DiagnoseResultResponse[]))]
|
||||||
[JsonSerializable(typeof(UserResponse))]
|
[JsonSerializable(typeof(UserResponse))]
|
||||||
[JsonSerializable(typeof(SystemInfoResponse))]
|
[JsonSerializable(typeof(SystemInfoResponse))]
|
||||||
[JsonSerializable(typeof(PagedData<UserResponse>))]
|
[JsonSerializable(typeof(PagedData<UserResponse>))]
|
||||||
|
|||||||
Reference in New Issue
Block a user