Files
Daybreak/Daybreak.Core/Views/LogsView.razor

157 lines
6.1 KiB
Plaintext

<div class="logs-container">
@if (this.ViewModel.Loading)
{
<SpinnerWidget IsLoading=true />
}
else
{
<div class="title-bar-additional-btns-left">
<button class="title-bar-btn" @onclick="this.ViewModel.ArchiveLogs" title="Archive logs">
<FluentIcon Value="new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Archive()" />
</button>
<button class="title-bar-btn" @onclick="this.ViewModel.ClearLogs" title="Clear logs">
<FluentIcon Value="new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.Delete()" />
</button>
<button class="title-bar-btn" @onclick="this.ViewModel.ToggleAutoScroll" title="@(this.ViewModel.AutoScroll ? "Disable auto-scroll" : "Enable auto-scroll")">
@if (this.ViewModel.AutoScroll)
{
<FluentIcon Value="new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.ArrowDownload()" />
}
else
{
<FluentIcon Value="new Microsoft.FluentUI.AspNetCore.Components.Icons.Regular.Size16.ArrowDownloadOff()" />
}
</button>
</div>
<div class="stretch-container backdrop-panel">
<div class="logs-search-container">
<DebouncedTextField Style="width: 100%; font-size: var(--font-size-small);"
Placeholder="Filter logs..."
SearchTerm="@this.ViewModel.SearchTerm"
OnSearch="@this.ViewModel.SetSearchTerm" />
</div>
<div class="console-container" id="logs-console-container" tabindex="0">
<div class="console-output">
@foreach (var log in this.ViewModel.Logs)
{
<div class="console-entry">
@foreach(var token in log.Tokens)
{
switch (token.Type)
{
case LogTokenType.NewLine:
<br />
break;
case LogTokenType.Timestamp:
<span class="log-timestamp">@this.Highlight(token.Value)</span>
break;
case LogTokenType.Level:
<span class="log-level @(this.GetLogLevelClass(log.Log.Level))">@this.Highlight(token.Value)</span>
break;
case LogTokenType.Message:
<span class="log-message">@this.Highlight(token.Value)</span>
break;
case LogTokenType.Exception:
<span class="log-exception">@this.Highlight(token.Value)</span>
break;
case LogTokenType.Property:
<span class="log-property">@this.Highlight(token.Value)</span>
break;
default:
<span>@this.Highlight(token.Value)</span>
break;
}
}
</div>
}
</div>
@if (!this.ViewModel.Logs.Any())
{
<div class="no-logs">
<span>@(string.IsNullOrEmpty(this.ViewModel.SearchTerm) ? "No logs available" : "No logs match the current filter")</span>
</div>
}
</div>
</div>
}
</div>
@page "/logs"
@inherits ViewBase<LogsView, LogsViewModel>
@inject IJSRuntime jSRuntime;
@using Serilog.Events
@using static Daybreak.Services.Logging.StructuredLogFormatter
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (this.ViewModel.AutoScroll)
{
await this.ScrollToBottomAsync();
}
}
private async Task ScrollToBottomAsync()
{
try
{
await this.jSRuntime.InvokeVoidAsync("eval",
"document.getElementById('logs-console-container').scrollTop = document.getElementById('logs-console-container').scrollHeight");
}
catch
{
}
}
private string GetLogLevelClass(LogEventLevel level) => level switch
{
LogEventLevel.Information => ".info",
LogEventLevel.Warning => ".warning",
LogEventLevel.Error => ".error",
LogEventLevel.Fatal => ".critical",
_ => ".default"
};
private RenderFragment Highlight(string? text) => builder =>
{
var searchTerm = this.ViewModel.SearchTerm;
if (string.IsNullOrEmpty(text))
{
return;
}
if (string.IsNullOrEmpty(searchTerm))
{
builder.AddContent(0, text);
return;
}
var seq = 0;
var idx = 0;
while (idx < text.Length)
{
var matchIdx = text.IndexOf(searchTerm, idx, StringComparison.OrdinalIgnoreCase);
if (matchIdx < 0)
{
builder.AddContent(seq++, text[idx..]);
break;
}
if (matchIdx > idx)
{
builder.AddContent(seq++, text[idx..matchIdx]);
}
builder.OpenElement(seq++, "span");
builder.AddAttribute(seq++, "class", "log-match");
builder.AddContent(seq++, text.Substring(matchIdx, searchTerm.Length));
builder.CloseElement();
idx = matchIdx + searchTerm.Length;
}
};
}