Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
995 changes: 995 additions & 0 deletions src/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

namespace EventLogExpert.Components.Tests.TestUtils.Constants;

public sealed partial class Constants
{
public const string DebugLogErrorMessage = "Error message";
public const string DebugLogFirstMessage = "First message";
public const string DebugLogSecondMessage = "Second message";
public const string DebugLogTestMessage = "Test message";
public const int DebugLogTestThreadId = 12;
public const string DebugLogTestTimestamp = "2026-04-29T07:53:20.9321852-04:00";
public const string DebugLogThirdMessage = "Third message";
}
34 changes: 34 additions & 0 deletions src/EventLogExpert.Components.Tests/TestUtils/DebugLogUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// // Copyright (c) Microsoft Corporation.
// // Licensed under the MIT License.

using Microsoft.Extensions.Logging;

namespace EventLogExpert.Components.Tests.TestUtils;

public static class DebugLogUtils
{
public static string BuildLine(LogLevel level, string message) =>
$"[{Constants.Constants.DebugLogTestTimestamp}] [{Constants.Constants.DebugLogTestThreadId}] [{level}] {message}";

public static async IAsyncEnumerable<string> ToAsyncEnumerable(IEnumerable<string> lines)
{
foreach (var line in lines)
{
yield return line;

await Task.Yield();
}
}

public static async IAsyncEnumerable<string> YieldThenThrow(IEnumerable<string> lines, Exception exception)
{
foreach (var line in lines)
{
yield return line;

await Task.Yield();
}

throw exception;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
else if (Data.Evaluator == FilterEvaluator.MultiSelect)
{
<ValueSelect AriaLabelledBy="@($"{Id}_Value")" CssClass="input filter-multiselect-dropdown" T="string" @bind-Values="Data.Values" IsMultiSelect
EmptyText="All"
ToStringFunc="@(x => string.IsNullOrWhiteSpace(x) ? "All" : x)">
<ValueSelectItem T="string" ClearItem>All</ValueSelectItem>
@foreach (var item in Items)
Expand Down
12 changes: 11 additions & 1 deletion src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ public ValueSelectItem<T>? HighlightedItem
[Parameter]
public bool IsMultiSelect { get; set; }

/// <summary>
/// Text shown by a multi-select <see cref="ValueSelect{T}"/> when no values are selected.
/// Defaults to "Empty"; consumers should override with a domain-appropriate label such as
/// "All" when an empty selection means "no filter applied".
/// </summary>
[Parameter]
public string EmptyText { get; set; } = "Empty";
Comment thread
jschick04 marked this conversation as resolved.

private string? DisplayString
{
get
Expand All @@ -56,7 +64,7 @@ private string? DisplayString
return converter is null ? $"{Value}" : converter.Set(Value);
}

if (Values.Count <= 0) { return "Empty"; }
if (Values.Count <= 0) { return EmptyText; }

return converter is null ?
string.Join(", ", Values.Select(x => $"{x}")) :
Expand Down Expand Up @@ -105,6 +113,8 @@ public async ValueTask DisposeAsync()
}
}

public bool HasAnySelection => _selectedValues.Count > 0;

public bool IsItemSelected(T value) => _selectedValues.Contains(value);

public async Task OpenDropDown() => await JSRuntime.InvokeVoidAsync("openDropdown", _selectComponent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ private string? DisplayString

private bool IsHighlighted => _parent.HighlightedItem?.Equals(this) ?? false;

private bool IsSelected => _parent.IsItemSelected(Value);
private bool IsSelected => ClearItem ? !_parent.HasAnySelection : _parent.IsItemSelected(Value);

[CascadingParameter]
private ValueSelect<T> ValueSelect
Expand Down
98 changes: 85 additions & 13 deletions src/EventLogExpert.Components/Modals/DebugLogModal.razor
Original file line number Diff line number Diff line change
@@ -1,35 +1,107 @@
@inherits ModalBase<bool>
@using EventLogExpert.UI
@using Microsoft.Extensions.Logging

<ModalChrome AriaLabel="Debug Log"
CloseLabel="Exit"
Footer="FooterPreset.CloseOnly"
Height="60%"
InlineAlert="@CurrentInlineAlert"
MinWidth="50rem"
MaxWidth="100rem"
MinWidth="100rem"
Comment thread
jschick04 marked this conversation as resolved.
Comment thread
jschick04 marked this conversation as resolved.
Comment thread
jschick04 marked this conversation as resolved.
OnClose="OnCancelAsync"
OnDialogClosedByUser="HandleDialogClosedByUserAsync"
OnInlineAlertResolved="HandleInlineAlertResolvedAsync"
@ref="ChromeRef">
<ChildContent>
<div aria-busy="@(_hasLoaded ? "false" : "true")" aria-label="Debug log entries" aria-live="polite" class="flex-column-scroll" role="log">
@if (_data.Count > 0)
<div aria-label="Filters" class="debug-log-filter-bar" role="group">
<label>Level</label>
<ValueSelect AriaLabel="Level operator"
CssClass="input filter-dropdown"
T="FilterEvaluator"
Value="_levelOperator"
ValueChanged="HandleLevelOperatorChanged"
ToStringFunc="@(x => x.ToFullString())">
<ValueSelectItem T="FilterEvaluator" Value="FilterEvaluator.Equals" />
<ValueSelectItem T="FilterEvaluator" Value="FilterEvaluator.NotEqual" />
<ValueSelectItem T="FilterEvaluator" Value="FilterEvaluator.MultiSelect" />
</ValueSelect>
@if (_levelOperator == FilterEvaluator.MultiSelect)
{
@for (var i = _data.Count - 1; i >= 0; i--)
{
<text>@_data[i]</text>
<br />
}
<ValueSelect AriaLabel="Levels"
CssClass="input filter-dropdown debug-log-level-multi"
EmptyText="All"
IsMultiSelect
T="LogLevel"
Values="_multiLevels"
ValuesChanged="HandleMultiLevelsChanged"
ToStringFunc="@(x => x.ToString())">
<ValueSelectItem T="LogLevel" ClearItem>All</ValueSelectItem>
Comment thread
jschick04 marked this conversation as resolved.
Comment thread
jschick04 marked this conversation as resolved.
Comment thread
jschick04 marked this conversation as resolved.
@foreach (var level in s_logLevels)
{
<ValueSelectItem T="LogLevel" Value="level" />
}
</ValueSelect>
}
else if (_hasLoaded)
else
{
<text>Log is Empty...</text>
<ValueSelect AriaLabel="Level"
CssClass="input filter-dropdown"
T="LogLevel?"
Value="_singleLevel"
ValueChanged="HandleSingleLevelChanged"
ToStringFunc="@(x => x?.ToString() ?? "All")">
<ValueSelectItem T="LogLevel?" ClearItem>All</ValueSelectItem>
@foreach (var level in s_logLevels)
{
<ValueSelectItem T="LogLevel?" Value="level" />
}
</ValueSelect>
}
<input aria-label="Filter messages"
class="input debug-log-text-filter"
@oninput="HandleStringFilterInput"
placeholder="Filter messages..."
type="text"
value="@_pendingStringFilter" />
</div>
<div aria-busy="@(_hasLoaded ? "false" : "true")" aria-label="Debug log entries" class="debug-log-viewport" role="region" tabindex="0">
Comment thread
jschick04 marked this conversation as resolved.
<Virtualize Items="_displayedView" Context="line" ItemSize="@RowHeightPx" OverscanCount="20">
<ItemContent>
<div class="debug-log-row">@line</div>
Comment thread
jschick04 marked this conversation as resolved.
</ItemContent>
Comment thread
jschick04 marked this conversation as resolved.
Comment thread
jschick04 marked this conversation as resolved.
<EmptyContent>
@if (_hasLoaded)
{
<span>@(_entries.Count == 0 ? "Log is Empty..." : "No entries match filters.")</span>
}
</EmptyContent>
</Virtualize>
</div>
</ChildContent>
<ExtraFooterContent>
<div class="button-group">
<button class="button" @onclick="Refresh" type="button">Refresh</button>
<button class="button" @onclick="Clear" type="button">Clear</button>
<div class="debug-log-footer">
<div class="debug-log-footer-actions debug-log-footer-left">
<button class="button"
disabled="@(!_hasLoaded || _filteredEntryCount == 0)"
@onclick="HandleExportAsync"
type="button">
Export
</button>
<button class="button"
disabled="@(!_hasLoaded || _filteredEntryCount == 0)"
@onclick="HandleCopyAsync"
type="button">
Copy
</button>
</div>
<span aria-atomic="true" aria-live="polite" class="debug-log-footer-counter" role="status">
@($"{_filteredEntryCount:N0} of {_entries.Count:N0} {(_entries.Count == 1 ? "entry" : "entries")}")
</span>
<div class="debug-log-footer-actions debug-log-footer-right">
<button class="button" @onclick="Refresh" type="button">Refresh</button>
<button class="button" @onclick="Clear" type="button">Clear</button>
</div>
</div>
</ExtraFooterContent>
</ModalChrome>
Loading
Loading