Implemented SignalR scaling using redis. Improved diagnose report generator. Added SignalR debug card in Diagnose page
This commit is contained in:
@@ -30,6 +30,9 @@ public record AppConfiguration
|
|||||||
[YamlMember(Description = "\nSettings for open telemetry")]
|
[YamlMember(Description = "\nSettings for open telemetry")]
|
||||||
public OpenTelemetryData OpenTelemetry { get; set; } = new();
|
public OpenTelemetryData OpenTelemetry { get; set; } = new();
|
||||||
|
|
||||||
|
[YamlMember(Description = "\nConfiguration for the realtime communication solution SignalR")]
|
||||||
|
public SignalRData SignalR { get; set; } = new();
|
||||||
|
|
||||||
public static AppConfiguration CreateEmpty()
|
public static AppConfiguration CreateEmpty()
|
||||||
{
|
{
|
||||||
return new AppConfiguration()
|
return new AppConfiguration()
|
||||||
@@ -47,6 +50,15 @@ public record AppConfiguration
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record SignalRData
|
||||||
|
{
|
||||||
|
[YamlMember(Description =
|
||||||
|
"\nWhether to use redis (or any other redis compatible solution) to scale out SignalR hubs. This is required when using multiple api server replicas")]
|
||||||
|
public bool UseRedis { get; set; } = false;
|
||||||
|
|
||||||
|
public string RedisConnectionString { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
public record FilesData
|
public record FilesData
|
||||||
{
|
{
|
||||||
[YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]
|
[YamlMember(Description = "The maximum file size limit a combine operation is allowed to process")]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Moonlight.ApiServer.Extensions;
|
|||||||
|
|
||||||
public static class ZipArchiveExtensions
|
public static class ZipArchiveExtensions
|
||||||
{
|
{
|
||||||
public static async Task AddBinary(this ZipArchive archive, string name, byte[] bytes)
|
public static async Task AddBinaryAsync(this ZipArchive archive, string name, byte[] bytes)
|
||||||
{
|
{
|
||||||
var entry = archive.CreateEntry(name);
|
var entry = archive.CreateEntry(name);
|
||||||
await using var dataStream = entry.Open();
|
await using var dataStream = entry.Open();
|
||||||
@@ -14,13 +14,13 @@ public static class ZipArchiveExtensions
|
|||||||
await dataStream.FlushAsync();
|
await dataStream.FlushAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task AddText(this ZipArchive archive, string name, string content)
|
public static async Task AddTextAsync(this ZipArchive archive, string name, string content)
|
||||||
{
|
{
|
||||||
var data = Encoding.UTF8.GetBytes(content);
|
var data = Encoding.UTF8.GetBytes(content);
|
||||||
await archive.AddBinary(name, data);
|
await archive.AddBinaryAsync(name, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task AddFile(this ZipArchive archive, string name, string path)
|
public static async Task AddFileAsync(this ZipArchive archive, string name, string path)
|
||||||
{
|
{
|
||||||
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
var fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
|
||||||
|
|||||||
@@ -21,21 +21,16 @@ public class DiagnoseController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task Diagnose([FromBody] GenerateDiagnoseRequest request)
|
public async Task<ActionResult> Diagnose([FromBody] GenerateDiagnoseRequest request)
|
||||||
{
|
{
|
||||||
var stream = await DiagnoseService.GenerateDiagnose(request.Providers);
|
var stream = await DiagnoseService.GenerateDiagnoseAsync(request.Providers);
|
||||||
|
|
||||||
await Results.Stream(
|
return File(stream, "application/zip", "diagnose.zip");
|
||||||
stream,
|
|
||||||
contentType: "application/zip",
|
|
||||||
fileDownloadName: "diagnose.zip"
|
|
||||||
)
|
|
||||||
.ExecuteAsync(HttpContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("providers")]
|
[HttpGet("providers")]
|
||||||
public async Task<DiagnoseProvideResponse[]> GetProviders()
|
public async Task<ActionResult<DiagnoseProvideResponse[]>> GetProviders()
|
||||||
{
|
{
|
||||||
return await DiagnoseService.GetProviders();
|
return await DiagnoseService.GetProvidersAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
Moonlight.ApiServer/Http/Hubs/DiagnoseHub.cs
Normal file
14
Moonlight.ApiServer/Http/Hubs/DiagnoseHub.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Http.Hubs;
|
||||||
|
|
||||||
|
[Authorize(Policy = "permissions:admin.system.diagnose")]
|
||||||
|
public class DiagnoseHub : Hub
|
||||||
|
{
|
||||||
|
[HubMethodName("Ping")]
|
||||||
|
public async Task Ping()
|
||||||
|
{
|
||||||
|
await Clients.All.SendAsync("Pong");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Text.Json;
|
using MoonCore.Yaml;
|
||||||
using Moonlight.ApiServer.Configuration;
|
using Moonlight.ApiServer.Configuration;
|
||||||
using Moonlight.ApiServer.Extensions;
|
using Moonlight.ApiServer.Extensions;
|
||||||
using Moonlight.ApiServer.Interfaces;
|
using Moonlight.ApiServer.Interfaces;
|
||||||
@@ -8,11 +9,11 @@ namespace Moonlight.ApiServer.Implementations.Diagnose;
|
|||||||
|
|
||||||
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
||||||
{
|
{
|
||||||
private readonly AppConfiguration Config;
|
private readonly AppConfiguration Configuration;
|
||||||
|
|
||||||
public CoreConfigDiagnoseProvider(AppConfiguration config)
|
public CoreConfigDiagnoseProvider(AppConfiguration configuration)
|
||||||
{
|
{
|
||||||
Config = config;
|
Configuration = configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string CheckForNullOrEmpty(string? content)
|
private string CheckForNullOrEmpty(string? content)
|
||||||
@@ -22,29 +23,25 @@ public class CoreConfigDiagnoseProvider : IDiagnoseProvider
|
|||||||
: "ISNOTEMPTY";
|
: "ISNOTEMPTY";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ModifyZipArchive(ZipArchive archive)
|
public async Task ModifyZipArchiveAsync(ZipArchive archive)
|
||||||
{
|
{
|
||||||
var json = JsonSerializer.Serialize(Config);
|
try
|
||||||
var config = JsonSerializer.Deserialize<AppConfiguration>(json);
|
|
||||||
|
|
||||||
if (config == null)
|
|
||||||
{
|
{
|
||||||
await archive.AddText("core/config.txt", "Could not fetch config.");
|
var configString = YamlSerializer.Serialize(Configuration);
|
||||||
return;
|
var configuration = YamlSerializer.Deserialize<AppConfiguration>(configString);
|
||||||
|
|
||||||
|
configuration.Database.Password = CheckForNullOrEmpty(configuration.Database.Password);
|
||||||
|
configuration.Authentication.Secret = CheckForNullOrEmpty(configuration.Authentication.Secret);
|
||||||
|
configuration.SignalR.RedisConnectionString = CheckForNullOrEmpty(configuration.SignalR.RedisConnectionString);
|
||||||
|
|
||||||
|
await archive.AddTextAsync(
|
||||||
|
"core/config.txt",
|
||||||
|
YamlSerializer.Serialize(configuration)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
await archive.AddTextAsync("core/config.txt", $"Unable to load config: {e.ToStringDemystified()}");
|
||||||
}
|
}
|
||||||
|
|
||||||
config.Database.Password = CheckForNullOrEmpty(config.Database.Password);
|
|
||||||
config.Authentication.Secret = CheckForNullOrEmpty(config.Authentication.Secret);
|
|
||||||
|
|
||||||
await archive.AddText(
|
|
||||||
"core/config.txt",
|
|
||||||
JsonSerializer.Serialize(
|
|
||||||
config,
|
|
||||||
new JsonSerializerOptions()
|
|
||||||
{
|
|
||||||
WriteIndented = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,17 +6,16 @@ namespace Moonlight.ApiServer.Implementations.Diagnose;
|
|||||||
|
|
||||||
public class LogsDiagnoseProvider : IDiagnoseProvider
|
public class LogsDiagnoseProvider : IDiagnoseProvider
|
||||||
{
|
{
|
||||||
public async Task ModifyZipArchive(ZipArchive archive)
|
public async Task ModifyZipArchiveAsync(ZipArchive archive)
|
||||||
{
|
{
|
||||||
var path = Path.Combine("storage", "logs", "latest.log");
|
var path = Path.Combine("storage", "logs", "moonlight.log");
|
||||||
|
|
||||||
if (!File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
await archive.AddText("logs.txt", "Logs file latest.log has not been found");
|
var logsContent = await File.ReadAllTextAsync(path);
|
||||||
return;
|
await archive.AddTextAsync("logs.txt", logsContent);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var logsContent = await File.ReadAllTextAsync(path);
|
await archive.AddTextAsync("logs.txt", "Logs file moonlight.log has not been found");
|
||||||
await archive.AddText("logs.txt", logsContent);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,5 +4,5 @@ namespace Moonlight.ApiServer.Interfaces;
|
|||||||
|
|
||||||
public interface IDiagnoseProvider
|
public interface IDiagnoseProvider
|
||||||
{
|
{
|
||||||
public Task ModifyZipArchive(ZipArchive archive);
|
public Task ModifyZipArchiveAsync(ZipArchive archive);
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
|
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.20"/>
|
||||||
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
|
<PackageReference Include="Hangfire.Core" Version="1.8.20"/>
|
||||||
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
|
<PackageReference Include="Hangfire.EntityFrameworkCore" Version="0.7.0"/>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="9.0.9" />
|
||||||
<PackageReference Include="MoonCore" Version="1.9.7" />
|
<PackageReference Include="MoonCore" Version="1.9.7" />
|
||||||
<PackageReference Include="MoonCore.Extended" Version="1.3.7" />
|
<PackageReference Include="MoonCore.Extended" Version="1.3.7" />
|
||||||
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>
|
<PackageReference Include="MoonCore.PluginFramework.Generator" Version="1.0.2"/>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class DiagnoseService
|
|||||||
Logger = logger;
|
Logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<DiagnoseProvideResponse[]> GetProviders()
|
public Task<DiagnoseProvideResponse[]> GetProvidersAsync()
|
||||||
{
|
{
|
||||||
var availableProviders = new List<DiagnoseProvideResponse>();
|
var availableProviders = new List<DiagnoseProvideResponse>();
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ public class DiagnoseService
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MemoryStream> GenerateDiagnose(string[] requestedProviders)
|
public async Task<MemoryStream> GenerateDiagnoseAsync(string[] requestedProviders)
|
||||||
{
|
{
|
||||||
IDiagnoseProvider[] providers;
|
IDiagnoseProvider[] providers;
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ public class DiagnoseService
|
|||||||
|
|
||||||
foreach (var provider in providers)
|
foreach (var provider in providers)
|
||||||
{
|
{
|
||||||
await provider.ModifyZipArchive(zipArchive);
|
await provider.ModifyZipArchiveAsync(zipArchive);
|
||||||
}
|
}
|
||||||
|
|
||||||
zipArchive.Dispose();
|
zipArchive.Dispose();
|
||||||
|
|||||||
@@ -23,15 +23,18 @@ public partial class Startup
|
|||||||
// we want to use the ApiKey scheme for authenticating the request
|
// we want to use the ApiKey scheme for authenticating the request
|
||||||
options.ForwardDefaultSelector = context =>
|
options.ForwardDefaultSelector = context =>
|
||||||
{
|
{
|
||||||
if (!context.Request.Headers.TryGetValue("Authorization", out var authHeader))
|
var headers = context.Request.Headers;
|
||||||
return "Session";
|
|
||||||
|
|
||||||
var auth = authHeader.FirstOrDefault();
|
// For regular api calls
|
||||||
|
if (headers.ContainsKey("Authorization"))
|
||||||
|
return "ApiKey";
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(auth) || !auth.StartsWith("Bearer "))
|
// For websocket requests which cannot use the Authorization header
|
||||||
return "Session";
|
if (headers.Upgrade == "websocket" && headers.Connection == "Upgrade" && context.Request.Query.ContainsKey("access_token"))
|
||||||
|
return "ApiKey";
|
||||||
|
|
||||||
return "ApiKey";
|
// Regular user traffic/auth
|
||||||
|
return "Session";
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.AddJwtBearer("ApiKey", null, options =>
|
.AddJwtBearer("ApiKey", null, options =>
|
||||||
@@ -63,6 +66,16 @@ public partial class Startup
|
|||||||
|
|
||||||
if (!result)
|
if (!result)
|
||||||
context.Fail("API key has been deleted");
|
context.Fail("API key has been deleted");
|
||||||
|
},
|
||||||
|
|
||||||
|
OnMessageReceived = context =>
|
||||||
|
{
|
||||||
|
var accessToken = context.Request.Query["access_token"];
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(accessToken))
|
||||||
|
context.Token = accessToken;
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
25
Moonlight.ApiServer/Startup/Startup.SignalR.cs
Normal file
25
Moonlight.ApiServer/Startup/Startup.SignalR.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Moonlight.ApiServer.Http.Hubs;
|
||||||
|
|
||||||
|
namespace Moonlight.ApiServer.Startup;
|
||||||
|
|
||||||
|
public partial class Startup
|
||||||
|
{
|
||||||
|
public Task RegisterSignalR()
|
||||||
|
{
|
||||||
|
var signalRBuilder = WebApplicationBuilder.Services.AddSignalR();
|
||||||
|
|
||||||
|
if (Configuration.SignalR.UseRedis)
|
||||||
|
signalRBuilder.AddStackExchangeRedis(Configuration.SignalR.RedisConnectionString);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task MapSignalR()
|
||||||
|
{
|
||||||
|
WebApplication.MapHub<DiagnoseHub>("/api/admin/system/diagnose/ws");
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,7 @@ public partial class Startup
|
|||||||
await RegisterAuth();
|
await RegisterAuth();
|
||||||
await RegisterCors();
|
await RegisterCors();
|
||||||
await RegisterHangfire();
|
await RegisterHangfire();
|
||||||
|
await RegisterSignalR();
|
||||||
await HookPluginBuild();
|
await HookPluginBuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ public partial class Startup
|
|||||||
await HookPluginConfigure();
|
await HookPluginConfigure();
|
||||||
|
|
||||||
await MapBase();
|
await MapBase();
|
||||||
|
await MapSignalR();
|
||||||
await HookPluginEndpoints();
|
await HookPluginEndpoints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
|
<PackageReference Include="Blazor-ApexCharts" Version="6.0.2" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
||||||
<PackageReference Include="MoonCore" Version="1.9.7" />
|
<PackageReference Include="MoonCore" Version="1.9.7" />
|
||||||
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
|
<PackageReference Include="MoonCore.Blazor" Version="1.3.1" />
|
||||||
|
|||||||
62
Moonlight.Client/UI/Components/SignalRDebug.razor
Normal file
62
Moonlight.Client/UI/Components/SignalRDebug.razor
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
|
@using Microsoft.Extensions.DependencyInjection
|
||||||
|
|
||||||
|
@inject NavigationManager Navigation
|
||||||
|
@inject ToastService ToastService
|
||||||
|
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">SignalR</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p>
|
||||||
|
<a class="link" href="https://dotnet.microsoft.com/en-us/apps/aspnet/signalr">SignalR</a> is used by Moonlight to provide realtime communication to itself and other modules.
|
||||||
|
You can test the SignalR communication by pressing the button below. For scaled instances you need to configure a redis compatible server to be used by all your replicas in order
|
||||||
|
for all SignalR Hubs to be synced.
|
||||||
|
</p>
|
||||||
|
<div class="mt-5">
|
||||||
|
<LazyLoader Load="Load">
|
||||||
|
<WButton OnClick="OnClick" CssClasses="btn btn-primary">
|
||||||
|
Send broadcast
|
||||||
|
</WButton>
|
||||||
|
</LazyLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code
|
||||||
|
{
|
||||||
|
private HubConnection? Connection;
|
||||||
|
|
||||||
|
private async Task Load(LazyLoader lazyLoader)
|
||||||
|
{
|
||||||
|
await lazyLoader.UpdateText("Connecting to SignalR endpoint");
|
||||||
|
|
||||||
|
Connection = new HubConnectionBuilder()
|
||||||
|
.WithUrl(Navigation.ToAbsoluteUri("/api/admin/system/diagnose/ws"))
|
||||||
|
.AddJsonProtocol()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Connection.On(
|
||||||
|
"Pong",
|
||||||
|
async () => await ToastService.Success("Received broadcast")
|
||||||
|
);
|
||||||
|
|
||||||
|
await Connection.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnClick(WButton _)
|
||||||
|
{
|
||||||
|
if (Connection == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Connection.SendAsync("Ping");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (Connection != null) await Connection.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using MoonCore.Blazor.FlyonUi.Helpers
|
@using MoonCore.Blazor.FlyonUi.Helpers
|
||||||
@using MoonCore.Helpers
|
@using MoonCore.Helpers
|
||||||
|
@using Moonlight.Client.UI.Components
|
||||||
@using Moonlight.Shared.Http.Requests.Admin.Sys
|
@using Moonlight.Shared.Http.Requests.Admin.Sys
|
||||||
@using Moonlight.Shared.Http.Responses.Admin.Sys
|
@using Moonlight.Shared.Http.Responses.Admin.Sys
|
||||||
|
|
||||||
@@ -15,22 +16,19 @@
|
|||||||
<NavTabs Index="5" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
|
<NavTabs Index="5" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-2">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
<div class="col-span-2 md:col-span-1 card">
|
<div class="col-span-1 card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="card-title">Diagnose</span>
|
<span class="card-title">Report</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
If you're experiencing issues or need help via our Discord, you're in the right place here!
|
With the button below you can create a diagnose report containing all important information to troubleshoot your moonlight instance and its modules.
|
||||||
By pressing the button below, Moonlight will run all available diagnostic checks and package the results
|
The diagnose file is a zip containing different logs and censored config files which can be shared with our support on discord.
|
||||||
into a
|
If you only want to export specific parts of the diagnose report, click on "Advanced" and select the desired providers
|
||||||
downloadable zip file.
|
|
||||||
The report includes useful information about your system, plugins, and environment, making it easier to
|
|
||||||
identify problems or share with support.
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<WButton OnClick="GenerateDiagnose" CssClasses="btn btn-primary my-5">Generate diagnose</WButton>
|
<WButton OnClick="GenerateDiagnose" CssClasses="btn btn-primary my-5">Generate report</WButton>
|
||||||
|
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<a class="text-primary cursor-pointer flex items-center" @onclick:preventDefault
|
<a class="text-primary cursor-pointer flex items-center" @onclick:preventDefault
|
||||||
@@ -67,6 +65,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-span-1">
|
||||||
|
<SignalRDebug />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code
|
@code
|
||||||
|
|||||||
Reference in New Issue
Block a user