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 = 2000; private List _serviceEvents = []; private readonly List _batchBuffer = []; private readonly SemaphoreSlim _batchLock = new(1, 1); private ElementReference _consoleContainer; private bool _autoScroll = true; private CancellationTokenSource _cts = new(); private TimeZoneInfo _timeZone = TimeZoneInfo.Local; private Virtualize? _virtualize; [Parameter] public List InitialEvents { get; set; } = []; [Parameter] public IAsyncEnumerable? 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("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 batch; await _batchLock.WaitAsync(); try { if (_batchBuffer.Count == 0) continue; batch = new List(_batchBuffer); _batchBuffer.Clear(); } finally { _batchLock.Release(); } foreach (var serviceEvent in batch.Where(serviceEvent => !_serviceEvents.Contains(serviceEvent))) { _serviceEvents.Add(serviceEvent); } _lastBatchUpdate = DateTime.UtcNow; if (_virtualize != null) { await _virtualize.RefreshDataAsync(); } if (_autoScroll) { await JsRuntime.InvokeVoidAsync("scrollToBottom", _consoleContainer); } await InvokeAsync(StateHasChanged); _updateScheduled = false; break; } } private void OnUserScroll(WheelEventArgs e) { _ = UpdateAutoScroll(); } private async Task UpdateAutoScroll() { if (_consoleContainer.Context != null) { var scrollInfo = await JsRuntime.InvokeAsync("getScrollInfo", _consoleContainer); _autoScroll = scrollInfo.ScrollTop + scrollInfo.ClientHeight >= scrollInfo.ScrollHeight - 20; } } private ValueTask> VirtualizedItemsProvider(ItemsProviderRequest request) { var items = _serviceEvents.Skip(request.StartIndex).Take(request.Count); return ValueTask.FromResult(new ItemsProviderResult(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; } } }