[CHANGE] Reworked event console

This commit is contained in:
max
2025-09-10 18:19:36 +02:00
parent ef6ca0ee07
commit 9be6f5be89
16 changed files with 326 additions and 131 deletions

View File

@@ -0,0 +1,21 @@
@inject IJSRuntime JsRuntime
@implements IDisposable
<MudPaper Elevation="Elevation" Class="@Class" Style="@Style">
<MudStack Class="ml-2 mb-2" Spacing="2" Row>
<MudStack Spacing="1">
<MudText Typo="Typo.h5">Live service events</MudText>
<MudText Typo="Typo.caption">@($"{_serviceEvents.Count} events")</MudText>
</MudStack>
<MudSwitch @bind-Value="@_autoScroll">Auto-scroll</MudSwitch>
</MudStack>
<div @ref="@_consoleContainer" class="console-container" @onwheel="OnUserScroll">
<Virtualize ItemsProvider="VirtualizedItemsProvider" Context="serviceEvent">
<div class="log-line">
@TimeZoneInfo.ConvertTime(serviceEvent.DateUtc, _timeZone)&nbsp;
<span class="log-severity @GetLogClass(serviceEvent)">@serviceEvent.Severity</span>&nbsp;[<span style="color: #1565c0">@serviceEvent.Source</span>]&nbsp;
<span style="color: snow">@serviceEvent.Message</span>
</div>
</Virtualize>
</div>
</MudPaper>

View File

@@ -0,0 +1,155 @@
using DotBased.Logging;
using Manager.App.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;
namespace Manager.App.Components.Application.System;
public partial class EventConsole : ComponentBase
{
private const int BatchDelayMs = 1000;
private List<ServiceEvent> _serviceEvents = [];
private readonly List<ServiceEvent> _batchBuffer = [];
private readonly SemaphoreSlim _batchLock = new(1, 1);
private ElementReference _consoleContainer;
private bool _autoScroll = true;
private CancellationTokenSource _cts = new();
private TimeZoneInfo _timeZone = TimeZoneInfo.Local;
[Parameter]
public List<ServiceEvent> InitialEvents { get; set; } = [];
[Parameter]
public IAsyncEnumerable<ServiceEvent>? AsyncEnumerable { get; set; }
[Parameter]
public int Elevation { get; set; }
[Parameter]
public string? Class { get; set; }
[Parameter]
public string? Style { get; set; }
protected override async Task OnInitializedAsync()
{
_serviceEvents.AddRange(InitialEvents);
var jsTimeZone = await JsRuntime.InvokeAsync<string>("getUserTimeZone");
if (!string.IsNullOrEmpty(jsTimeZone))
{
_timeZone = TimeZoneInfo.FindSystemTimeZoneById(jsTimeZone);
}
_ = Task.Run(() => ReadEventStreamsAsync(_cts.Token));
}
private async Task ReadEventStreamsAsync(CancellationToken token)
{
if (AsyncEnumerable == null)
{
return;
}
await foreach (var serviceEvent in AsyncEnumerable.WithCancellation(token))
{
await _batchLock.WaitAsync(token);
try
{
_batchBuffer.Add(serviceEvent);
}
finally
{
_batchLock.Release();
}
_ = BatchUpdateUi();
}
}
private string GetLogClass(ServiceEvent serviceEvent) =>
serviceEvent.Severity switch
{
LogSeverity.Info => "log-info",
LogSeverity.Warning => "log-warning",
LogSeverity.Error => "log-error",
LogSeverity.Debug => "log-debug",
LogSeverity.Trace => "log-trace",
LogSeverity.Fatal => "log-fatal",
LogSeverity.Verbose => "log-error",
_ => "log-info"
};
private DateTime _lastBatchUpdate = DateTime.MinValue;
private bool _updateScheduled;
private async Task BatchUpdateUi()
{
if (_updateScheduled) return;
_updateScheduled = true;
while (!_cts.Token.IsCancellationRequested)
{
var elapsed = (DateTime.UtcNow - _lastBatchUpdate).TotalMilliseconds;
if (elapsed < BatchDelayMs)
{
await Task.Delay(BatchDelayMs - (int)elapsed, _cts.Token);
}
List<ServiceEvent> batch;
await _batchLock.WaitAsync();
try
{
if (_batchBuffer.Count == 0) continue;
batch = new List<ServiceEvent>(_batchBuffer);
_batchBuffer.Clear();
}
finally
{
_batchLock.Release();
}
_serviceEvents.AddRange(batch);
_lastBatchUpdate = DateTime.UtcNow;
await InvokeAsync(StateHasChanged);
if (_autoScroll)
{
await JsRuntime.InvokeVoidAsync("scrollToBottom", _consoleContainer);
}
_updateScheduled = false;
break;
}
}
private void OnUserScroll(WheelEventArgs e)
{
_ = UpdateAutoScroll();
}
private async Task UpdateAutoScroll()
{
if (_consoleContainer.Context != null)
{
var scrollInfo = await JsRuntime.InvokeAsync<ScrollInfo>("getScrollInfo", _consoleContainer);
_autoScroll = scrollInfo.ScrollTop + scrollInfo.ClientHeight >= scrollInfo.ScrollHeight - 20;
}
}
private ValueTask<ItemsProviderResult<ServiceEvent>> VirtualizedItemsProvider(ItemsProviderRequest request)
{
var items = _serviceEvents.Skip(request.StartIndex).Take(request.Count);
return ValueTask.FromResult(new ItemsProviderResult<ServiceEvent>(items, _serviceEvents.Count));
}
public void Dispose()
{
_batchLock.Dispose();
}
private class ScrollInfo
{
public double ScrollTop { get; set; }
public double ScrollHeight { get; set; }
public double ClientHeight { get; set; }
}
}

View File

@@ -0,0 +1,51 @@
.console-container {
background-color: #1e1e1e;
color: #9c9898;
padding: 10px;
border-radius: 8px;
flex: 1;
overflow-y: auto;
font-family: monospace;
}
.log-severity{
display: inline-block;
width: 8ch;
text-align: left;
font-weight: bold;
}
.log-line {
display: flex;
justify-content: start;
align-items: center;
white-space: nowrap;
}
.log-info {
color: #3f6b81;
}
.log-warning {
color: #f8f802;
}
.log-error {
color: #f44747;
}
.log-debug {
color: #e110ff;
}
.log-trace {
color: #535353;
}
.log-fatal {
color: #af1e1e;
}
.log-verbose {
color: #8085ff;
}