diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index 6a877e43..6182eb1e 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -2,9 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Banner; +using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Restart; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Menu; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -140,9 +142,9 @@ private void OnCyclePrev() private void OnDismissAttention() => BannerService.DismissAttention(); - private void OnDismissError(Guid id) => BannerService.DismissError(id); + private void OnDismissError(BannerId id) => BannerService.DismissError(id); - private void OnDismissInfo(Guid id) => BannerService.DismissInfoBanner(id); + private void OnDismissInfo(BannerId id) => BannerService.DismissInfoBanner(id); private async Task OnErrorActionClickedAsync(Func action) { @@ -178,7 +180,7 @@ private async Task OnOpenSettingsClickedAsync() { TraceLogger.Error($"{nameof(BannerHost)}.{nameof(OnOpenSettingsClickedAsync)}: open settings threw: {ex}"); - Guid errorId = BannerService.ReportError("Settings", $"Failed to open settings: {ex.Message}"); + BannerId errorId = BannerService.ReportError("Settings", $"Failed to open settings: {ex.Message}"); _selectedItem = new BannerCycleItem(BannerView.Error, 0, errorId); return; @@ -188,7 +190,7 @@ private async Task OnOpenSettingsClickedAsync() { TraceLogger.Error($"{nameof(BannerHost)}.{nameof(OnOpenSettingsClickedAsync)}: open settings returned false"); - Guid errorId = BannerService.ReportError("Settings", "Failed to open settings; try again from the menu."); + BannerId errorId = BannerService.ReportError("Settings", "Failed to open settings; try again from the menu."); _selectedItem = new BannerCycleItem(BannerView.Error, 0, errorId); } } diff --git a/src/EventLogExpert.Components/Base/InputComponent.cs b/src/EventLogExpert.Components/Base/InputComponent.cs index a18a882b..6efb328b 100644 --- a/src/EventLogExpert.Components/Base/InputComponent.cs +++ b/src/EventLogExpert.Components/Base/InputComponent.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Common.Display; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Base; diff --git a/src/EventLogExpert.Components/Base/ModalBase.cs b/src/EventLogExpert.Components/Base/ModalBase.cs index f993d03f..976f7a78 100644 --- a/src/EventLogExpert.Components/Base/ModalBase.cs +++ b/src/EventLogExpert.Components/Base/ModalBase.cs @@ -1,7 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Modal; using Fluxor.Blazor.Web.Components; using Microsoft.AspNetCore.Components; @@ -18,6 +19,8 @@ public abstract class ModalBase : FluxorComponent, IInlineAlertHost private InlineAlertEntry? _activeInlineAlert; private long _modalId; + [Inject] internal IInlineAlertHostBroker InlineAlertHostBroker { get; init; } = null!; + [Inject] internal IModalService ModalService { get; init; } = null!; /// Bound by concrete modals via @ref on their . @@ -66,17 +69,6 @@ public Task ShowInlineAlertAsync(InlineAlertRequest request, return tcs.Task; } - // Route Esc/native-close through OnCancelAsync so all close paths share the same pipeline. - // Protected so derived modals' .razor markup can wire OnDialogClosedByUser="HandleDialogClosedByUserAsync" - // even when the derived class lives in a different assembly than ModalBase. - protected Task HandleDialogClosedByUserAsync() => OnCancelAsync(); - - protected Task HandleInlineAlertResolvedAsync(InlineAlertResult result) - { - TryClearInlineAlert(null, result, false); - return Task.CompletedTask; - } - protected async Task CompleteAsync(TResult? result) { await OnClosingAsync(); @@ -105,7 +97,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) pending?.CancellationRegistration.Dispose(); pending?.Tcs.TrySetCanceled(); - ModalService.UnregisterActiveAlertHost(_modalId); + InlineAlertHostBroker.Unregister(_modalId); // Defensive: complete the task if we were torn down without an explicit close path. // Stale ids are ignored, so this is a no-op when Complete already ran. @@ -115,6 +107,18 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) await base.DisposeAsyncCore(disposing); } + // Route Esc/native-close through OnCancelAsync so all close paths share the same pipeline. + // Protected so derived modals' .razor markup can wire OnDialogClosedByUser="HandleDialogClosedByUserAsync" + // even when the derived class lives in a different assembly than ModalBase. + protected Task HandleDialogClosedByUserAsync() => OnCancelAsync(); + + protected Task HandleInlineAlertResolvedAsync(InlineAlertResult result) + { + TryClearInlineAlert(null, result, false); + + return Task.CompletedTask; + } + protected virtual Task OnAcceptAsync() => CompleteAsync(default); protected virtual Task OnCancelAsync() => CompleteAsync(default); @@ -130,7 +134,7 @@ protected override void OnInitialized() { // Capture the active id so a stale modal can never complete a successor's task. _modalId = ModalService.ActiveModalId; - ModalService.RegisterActiveAlertHost(_modalId, this); + InlineAlertHostBroker.Register(_modalId, this); base.OnInitialized(); } diff --git a/src/EventLogExpert.Components/Base/ModalChrome.razor.cs b/src/EventLogExpert.Components/Base/ModalChrome.razor.cs index bf4151c6..43556428 100644 --- a/src/EventLogExpert.Components/Base/ModalChrome.razor.cs +++ b/src/EventLogExpert.Components/Base/ModalChrome.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Alerts; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; @@ -174,17 +174,6 @@ private async Task FocusInlineAlertElementAsync() } } - private void ResetInlineAlertPromptValueIfChanged() - { - if (ReferenceEquals(_inlineAlertInitializedFor, InlineAlert)) { return; } - - _inlineAlertInitializedFor = InlineAlert; - - _inlineAlertPromptValue = InlineAlert is { IsPrompt: true } - ? InlineAlert.PromptInitialValue ?? string.Empty - : string.Empty; - } - private Task HandleAcceptAsync() => OnAccept.InvokeAsync(); private Task HandleCancelButtonAsync() => OnCancel.InvokeAsync(); @@ -243,4 +232,15 @@ private Task HandleInlineAlertCancelAsync() => OnInlineAlertResolved.InvokeAsync(new InlineAlertResult(false, null)); private Task HandleSaveAsync() => OnSave.InvokeAsync(); + + private void ResetInlineAlertPromptValueIfChanged() + { + if (ReferenceEquals(_inlineAlertInitializedFor, InlineAlert)) { return; } + + _inlineAlertInitializedFor = InlineAlert; + + _inlineAlertPromptValue = InlineAlert is { IsPrompt: true } + ? InlineAlert.PromptInitialValue ?? string.Empty + : string.Empty; + } } diff --git a/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs b/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs index 50416eb2..aaefed74 100644 --- a/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs @@ -1,8 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Database; @@ -10,10 +9,10 @@ namespace EventLogExpert.Components.Database; public sealed partial class DatabaseEntryRow : ComponentBase { /// - /// Click-driven reveal flag for the trash strip on the left of the row. Set true when the - /// name button is clicked, cleared when the cursor leaves the row -- so re-entering the row - /// without re-clicking does not re-open the slide, even though the name button may still - /// hold DOM focus. Keyboard navigation drives the reveal via :focus-visible in CSS instead. + /// Click-driven reveal flag for the trash strip on the left of the row. Set true when the name button is clicked, + /// cleared when the cursor leaves the row -- so re-entering the row without re-clicking does not re-open the slide, + /// even though the name button may still hold DOM focus. Keyboard navigation drives the reveal via :focus-visible in + /// CSS instead. /// private bool _isMouseRevealed; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor index 2aa55e40..e8e6d6b7 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor @@ -1,4 +1,3 @@ -@using EventLogExpert.Components.Base @if (!_isDismissed && _entries.Count > 0) { _promptedFor = new(StringComparer.OrdinalIgnoreCase); - private Guid? _recoveryBannerId; + private BannerId? _recoveryBannerId; [Inject] private IBannerService BannerService { get; init; } = null!; @@ -43,6 +43,14 @@ protected override void OnInitialized() EvaluateState(); } + private void DismissCurrentBannerIfAny() + { + if (_recoveryBannerId is not { } activeId) { return; } + + BannerService.DismissError(activeId); + _recoveryBannerId = null; + } + private void EvaluateState() { if (_disposed) { return; } @@ -68,14 +76,6 @@ private void EvaluateState() _promptedFor = currentBackupSet; } - private void DismissCurrentBannerIfAny() - { - if (_recoveryBannerId is not { } activeId) { return; } - - BannerService.DismissError(activeId); - _recoveryBannerId = null; - } - private void HandleBannerStateChanged() { if (_disposed) { return; } @@ -112,7 +112,7 @@ private Task OpenRecoveryDialogAsync() }); } - private Guid ReportRecoveryBanner(int count) + private BannerId ReportRecoveryBanner(int count) { string message = count == 1 ? "1 database needs recovery from interrupted upgrade." diff --git a/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs b/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs index e3fb6336..8f1490e9 100644 --- a/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs +++ b/src/EventLogExpert.Components/Database/SettingsUpgradeProgressBanner.razor.cs @@ -2,8 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Banner; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Database; diff --git a/src/EventLogExpert.Components/Filters/AdvancedFilterRow.razor.cs b/src/EventLogExpert.Components/Filters/AdvancedFilterRow.razor.cs index 2a97b8d2..5c268fa2 100644 --- a/src/EventLogExpert.Components/Filters/AdvancedFilterRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/AdvancedFilterRow.razor.cs @@ -2,8 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Filters.Base; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterPane; namespace EventLogExpert.Components.Filters; @@ -13,15 +13,15 @@ protected override void DispatchRemoveFilter() { if (Value is not { } savedFilter) { return; } - Dispatcher.Dispatch(new FilterPaneAction.RemoveFilter(savedFilter.Id)); + Dispatcher.Dispatch(new RemoveFilterAction(savedFilter.Id)); } - protected override void DispatchSetFilter(FilterModel filter) => - Dispatcher.Dispatch(new FilterPaneAction.SetFilter(filter)); + protected override void DispatchSetFilter(SavedFilter filter) => + Dispatcher.Dispatch(new SetFilterAction(filter)); protected override void DispatchToggleEnabled(FilterId id) => - Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterEnabled(id)); + Dispatcher.Dispatch(new ToggleFilterEnabledAction(id)); protected override void DispatchToggleExclusion(FilterId id) => - Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterExcluded(id)); + Dispatcher.Dispatch(new ToggleFilterExcludedAction(id)); } diff --git a/src/EventLogExpert.Components/Filters/Base/EditableFilterRowBase.cs b/src/EventLogExpert.Components/Filters/Base/EditableFilterRowBase.cs index 8ecd4f3f..99e552db 100644 --- a/src/EventLogExpert.Components/Filters/Base/EditableFilterRowBase.cs +++ b/src/EventLogExpert.Components/Filters/Base/EditableFilterRowBase.cs @@ -1,21 +1,18 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Filter; using Microsoft.AspNetCore.Components; using IDispatcher = Fluxor.IDispatcher; namespace EventLogExpert.Components.Filters.Base; /// -/// Base for filter rows that bind either to a saved (via -/// ) or to a parent-owned draft (via +/// Base for filter rows that bind either to a saved (via +/// ) or to a parent-owned draft (via /// ). Exactly one must be set. /// -public abstract class EditableFilterRowBase : FilterRowBase +public abstract class EditableFilterRowBase : FilterRowBase { /// Notifies the parent which saved rows are mid-edit. [Parameter] public EventCallback<(FilterId Id, bool IsEditing)> OnEditingChanged { get; set; } @@ -24,16 +21,16 @@ public abstract class EditableFilterRowBase : FilterRowBase [Parameter] public EventCallback OnPendingDiscard { get; set; } /// Pending-row save: parent must remove the draft and dispatch the upsert atomically. - [Parameter] public EventCallback OnPendingSave { get; set; } + [Parameter] public EventCallback OnPendingSave { get; set; } /// Mutually exclusive with . - [Parameter] public FilterDraftModel? PendingDraft { get; set; } + [Parameter] public FilterDraft? PendingDraft { get; set; } [Inject] protected IDispatcher Dispatcher { get; init; } = null!; protected string ErrorMessage { get; set; } = string.Empty; - protected FilterDraftModel? Filter { get; set; } + protected FilterDraft? Filter { get; set; } [Inject] protected IFilterService FilterService { get; init; } = null!; @@ -56,13 +53,13 @@ protected async Task CancelFilter() await OnEditingChanged.InvokeAsync((savedFilter.Id, false)); } - protected Task CommitPendingAsync(FilterModel filter) => OnPendingSave.InvokeAsync(filter); + protected Task CommitPendingAsync(SavedFilter filter) => OnPendingSave.InvokeAsync(filter); /// Saved rows only. protected abstract void DispatchRemoveFilter(); /// Dispatches the appropriate set-filter action (FilterPane, FilterGroup, etc.). - protected abstract void DispatchSetFilter(FilterModel filter); + protected abstract void DispatchSetFilter(SavedFilter filter); /// Default is no-op; subclasses with an enable/disable toggle override. protected virtual void DispatchToggleEnabled(FilterId id) { } @@ -77,7 +74,7 @@ protected async Task EditFilter() if (Value is not { } savedFilter) { return; } OnEditSessionResetting(); - Filter = FilterDraftModel.FromFilterModel(savedFilter); + Filter = FilterDraft.FromSavedFilter(savedFilter); await OnEditingChanged.InvokeAsync((savedFilter.Id, true)); } @@ -173,27 +170,27 @@ protected void ToggleFilterExclusion() } /// - /// Validates the draft and produces the immutable to dispatch. - /// Returning aborts the save (subclass surfaces the error). Default - /// enforces non-empty text and compiles via . + /// Validates the draft and produces the immutable to dispatch. Returning + /// aborts the save (subclass surfaces the error). Default enforces non-empty text and compiles + /// via . /// - protected virtual ValueTask TrySaveAsync(FilterDraftModel draft) + protected virtual ValueTask TrySaveAsync(FilterDraft draft) { if (string.IsNullOrWhiteSpace(draft.ComparisonText)) { ErrorMessage = "Cannot save an empty filter"; - return ValueTask.FromResult(null); + return ValueTask.FromResult(null); } if (!FilterCompiler.TryCompile(draft.ComparisonText, out var compiled, out var error)) { ErrorMessage = error; - return ValueTask.FromResult(null); + return ValueTask.FromResult(null); } - var result = new FilterModel + var result = new SavedFilter { Id = draft.Id, Color = draft.Color, @@ -205,6 +202,6 @@ protected void ToggleFilterExclusion() IsExcluded = draft.IsExcluded }; - return ValueTask.FromResult(result); + return ValueTask.FromResult(result); } } diff --git a/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs index abe0518f..dbf39b68 100644 --- a/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs @@ -2,10 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Filters.Base; -using EventLogExpert.UI; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.FilterCache; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterCache; +using EventLogExpert.UI.FilterPane; using Fluxor; using Microsoft.AspNetCore.Components; @@ -29,15 +28,15 @@ protected override void DispatchRemoveFilter() { if (Value is not { } savedFilter) { return; } - Dispatcher.Dispatch(new FilterPaneAction.RemoveFilter(savedFilter.Id)); + Dispatcher.Dispatch(new RemoveFilterAction(savedFilter.Id)); } - protected override void DispatchSetFilter(FilterModel filter) => - Dispatcher.Dispatch(new FilterPaneAction.SetFilter(filter)); + protected override void DispatchSetFilter(SavedFilter filter) => + Dispatcher.Dispatch(new SetFilterAction(filter)); protected override void DispatchToggleEnabled(FilterId id) => - Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterEnabled(id)); + Dispatcher.Dispatch(new ToggleFilterEnabledAction(id)); protected override void DispatchToggleExclusion(FilterId id) => - Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterExcluded(id)); + Dispatcher.Dispatch(new ToggleFilterExcludedAction(id)); } diff --git a/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor b/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor index d0944d96..8390b2ec 100644 --- a/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor +++ b/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor @@ -1,5 +1,4 @@ @using EventLogExpert.UI - @foreach (FilterCategory item in Enum.GetValues(typeof(FilterCategory))) { diff --git a/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor.cs b/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor.cs index 670341e1..89959582 100644 --- a/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterCategoryEditor.razor.cs @@ -1,10 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; -using EventLogExpert.UI.Store.EventLog; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Filter; using Fluxor; using Microsoft.AspNetCore.Components; using System.Collections.Immutable; diff --git a/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs index 764564bc..95c38c4a 100644 --- a/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs @@ -2,9 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Filters.Base; -using EventLogExpert.UI; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.FilterGroup; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterGroup; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Filters; @@ -17,16 +16,16 @@ protected override void DispatchRemoveFilter() { if (Value is not { } savedFilter) { return; } - Dispatcher.Dispatch(new FilterGroupAction.RemoveFilter(ParentId, savedFilter.Id)); + Dispatcher.Dispatch(new RemoveFilterAction(ParentId, savedFilter.Id)); } - protected override void DispatchSetFilter(FilterModel filter) => - Dispatcher.Dispatch(new FilterGroupAction.SetFilter(ParentId, filter)); + protected override void DispatchSetFilter(SavedFilter filter) => + Dispatcher.Dispatch(new SetFilterAction(ParentId, filter)); protected override void DispatchToggleExclusion(FilterId id) => - Dispatcher.Dispatch(new FilterGroupAction.ToggleFilterExcluded(ParentId, id)); + Dispatcher.Dispatch(new ToggleFilterExcludedAction(ParentId, id)); - protected override async ValueTask TrySaveAsync(FilterDraftModel draft) + protected override async ValueTask TrySaveAsync(FilterDraft draft) { var compiled = await base.TrySaveAsync(draft); diff --git a/src/EventLogExpert.Components/Filters/FilterRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterRow.razor.cs index 22546e61..37761543 100644 --- a/src/EventLogExpert.Components/Filters/FilterRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterRow.razor.cs @@ -2,10 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Filters.Base; -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterPane; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Filters; @@ -18,26 +17,26 @@ protected override void DispatchRemoveFilter() { if (Value is not { } savedFilter) { return; } - Dispatcher.Dispatch(new FilterPaneAction.RemoveFilter(savedFilter.Id)); + Dispatcher.Dispatch(new RemoveFilterAction(savedFilter.Id)); } - protected override void DispatchSetFilter(FilterModel filter) => - Dispatcher.Dispatch(new FilterPaneAction.SetFilter(filter)); + protected override void DispatchSetFilter(SavedFilter filter) => + Dispatcher.Dispatch(new SetFilterAction(filter)); protected override void DispatchToggleEnabled(FilterId id) => - Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterEnabled(id)); + Dispatcher.Dispatch(new ToggleFilterEnabledAction(id)); protected override void DispatchToggleExclusion(FilterId id) => - Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterExcluded(id)); + Dispatcher.Dispatch(new ToggleFilterExcludedAction(id)); /// Structured filter: validate via TryParse and surface failures via the alert dialog (no inline banner). - protected override async ValueTask TrySaveAsync(FilterDraftModel draft) + protected override async ValueTask TrySaveAsync(FilterDraft draft) { var basicFilter = draft.ToBasicFilter(); if (FilterService.TryParse(basicFilter, out string comparisonString)) { - var model = FilterModel.TryCreate( + var model = SavedFilter.TryCreate( comparisonString, FilterType.Basic, basicFilter, diff --git a/src/EventLogExpert.Components/Filters/FilterRowChrome.razor.cs b/src/EventLogExpert.Components/Filters/FilterRowChrome.razor.cs index 3a2efbc8..8dcedc67 100644 --- a/src/EventLogExpert.Components/Filters/FilterRowChrome.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterRowChrome.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Filters; @@ -20,7 +20,7 @@ public sealed partial class FilterRowChrome : ComponentBase [Parameter] public RenderFragment? ExtraEditingButtons { get; set; } - [Parameter] public FilterDraftModel? Filter { get; set; } + [Parameter] public FilterDraft? Filter { get; set; } [Parameter] public bool IsPending { get; set; } @@ -44,5 +44,5 @@ public sealed partial class FilterRowChrome : ComponentBase [Parameter] public bool UseInlineErrorRow { get; set; } - [Parameter] public FilterModel? Value { get; set; } + [Parameter] public SavedFilter? Value { get; set; } } diff --git a/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs b/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs index 57d8db5a..1e268553 100644 --- a/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/SubFilterRow.razor.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Filters.Base; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Filters; diff --git a/src/EventLogExpert.Components/Inputs/BooleanSelect.razor b/src/EventLogExpert.Components/Inputs/BooleanSelect.razor index bf39997a..e3d11dcf 100644 --- a/src/EventLogExpert.Components/Inputs/BooleanSelect.razor +++ b/src/EventLogExpert.Components/Inputs/BooleanSelect.razor @@ -1,4 +1,3 @@ -@using EventLogExpert.Components.Base @inherits InputComponent
diff --git a/src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs b/src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs index c2793718..d8a3f17f 100644 --- a/src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs +++ b/src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; +using EventLogExpert.UI.Common.Display; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; @@ -27,6 +28,16 @@ public sealed partial class ValueSelect : InputComponent, IAsyncDisposable [Parameter] public string? DataHighlight { get; set; } + /// + /// Text shown by a multi-select 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". + /// + [Parameter] + public string EmptyText { get; set; } = "Empty"; + + public bool HasAnySelection => _selectedValues.Count > 0; + public ValueSelectItem? HighlightedItem { get => _highlightedItem; @@ -45,14 +56,6 @@ public ValueSelectItem? HighlightedItem [Parameter] public bool IsMultiSelect { get; set; } - /// - /// Text shown by a multi-select 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". - /// - [Parameter] - public string EmptyText { get; set; } = "Empty"; - private string? DisplayString { get @@ -113,8 +116,6 @@ 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); diff --git a/src/EventLogExpert.Components/Inputs/ValueSelectItem.razor.cs b/src/EventLogExpert.Components/Inputs/ValueSelectItem.razor.cs index 33c0a703..06ce8216 100644 --- a/src/EventLogExpert.Components/Inputs/ValueSelectItem.razor.cs +++ b/src/EventLogExpert.Components/Inputs/ValueSelectItem.razor.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Common.Display; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Inputs; diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index 670c52b0..fb0b5f62 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -2,12 +2,12 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Channels; -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; -using EventLogExpert.UI.Store.EventLog; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Versioning; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.FilterPane; +using EventLogExpert.UI.Menu; +using EventLogExpert.UI.Settings; using Fluxor; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -49,7 +49,7 @@ public void Dispose() { InvalidatePendingOpen(); - Settings.CopyTypeChanged -= OnSettingsChanged; + Settings.CopyFormatChanged -= OnSettingsChanged; MenuService.StateChanged -= OnMenuServiceStateChanged; MenuService.NavigateBarRequested -= OnNavigateBarRequested; } @@ -60,7 +60,7 @@ protected override void OnInitialized() FilterPaneIsEnabled.Select(state => state.IsEnabled); HasActiveLogs.Select(state => !state.ActiveLogs.IsEmpty); - Settings.CopyTypeChanged += OnSettingsChanged; + Settings.CopyFormatChanged += OnSettingsChanged; MenuService.StateChanged += OnMenuServiceStateChanged; MenuService.NavigateBarRequested += OnNavigateBarRequested; @@ -74,22 +74,22 @@ protected override void OnInitialized() private IReadOnlyList BuildEdit() { - var copyShortcut = Settings.CopyType; + var defaultCopyFormat = Settings.CopyFormat; return [ MenuItem.Item("Copy Selected", - () => Actions.CopySelectedAsync(CopyType.Default), - copyShortcut == CopyType.Default ? "Ctrl+C" : null), + () => Actions.CopySelectedAsync(EventCopyFormat.Default), + defaultCopyFormat == EventCopyFormat.Default ? "Ctrl+C" : null), MenuItem.Item("Copy Selected (Simple)", - () => Actions.CopySelectedAsync(CopyType.Simple), - copyShortcut == CopyType.Simple ? "Ctrl+C" : null), + () => Actions.CopySelectedAsync(EventCopyFormat.Simple), + defaultCopyFormat == EventCopyFormat.Simple ? "Ctrl+C" : null), MenuItem.Item("Copy Selected (XML)", - () => Actions.CopySelectedAsync(CopyType.Xml), - copyShortcut == CopyType.Xml ? "Ctrl+C" : null), + () => Actions.CopySelectedAsync(EventCopyFormat.Xml), + defaultCopyFormat == EventCopyFormat.Xml ? "Ctrl+C" : null), MenuItem.Item("Copy Selected (Full)", - () => Actions.CopySelectedAsync(CopyType.Full), - copyShortcut == CopyType.Full ? "Ctrl+C" : null), + () => Actions.CopySelectedAsync(EventCopyFormat.Full), + defaultCopyFormat == EventCopyFormat.Full ? "Ctrl+C" : null), MenuItem.Separator(), MenuItem.Item("Save All Filters", () => Actions.SaveAllFiltersAsync()), MenuItem.Item("Clear All Filters", Actions.ClearAllFilters), diff --git a/src/EventLogExpert.Components/Menu/MenuHost.razor.cs b/src/EventLogExpert.Components/Menu/MenuHost.razor.cs index a7c32e9a..0d14197e 100644 --- a/src/EventLogExpert.Components/Menu/MenuHost.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuHost.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; diff --git a/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs b/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs index 946b6403..9e432249 100644 --- a/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Menu; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.JSInterop; @@ -16,9 +16,9 @@ public sealed partial class MenuRenderer // Type-ahead matches reset after this idle window (WAI-ARIA Authoring Practices guidance). private static readonly TimeSpan s_typeAheadResetWindow = TimeSpan.FromMilliseconds(500); + private bool _focusOnNextRender; private int _focusedIndex = -1; - private bool _focusOnNextRender; private ElementReference[] _itemElements = []; private MenuItem? _openItem; private bool _openSubmenuFocusesFirstChild; @@ -47,16 +47,16 @@ public sealed partial class MenuRenderer [Parameter] public EventCallback OnCloseSubmenu { get; set; } /// - /// Raised when the top-level menu wants the menubar to switch to the previous (-1) or next (+1) bar item. - /// Hosts that don't sit on a menubar can ignore this. + /// Raised when the top-level menu wants the menubar to switch to the previous (-1) or next (+1) bar item. Hosts + /// that don't sit on a menubar can ignore this. /// [Parameter] public EventCallback OnNavigateBar { get; set; } /// - /// When true, the renderer will not set _focusedIndex from on - /// parameter changes and will not auto-focus the first enabled item on first render. Used for hover-opened - /// submenus so keyboard focus stays on the parent item per WAI-ARIA menu guidance — focus only enters the - /// submenu when the user explicitly opens it via Enter/Space/ArrowRight. + /// When true, the renderer will not set _focusedIndex from on parameter + /// changes and will not auto-focus the first enabled item on first render. Used for hover-opened submenus so keyboard + /// focus stays on the parent item per WAI-ARIA menu guidance — focus only enters the submenu when the user explicitly + /// opens it via Enter/Space/ArrowRight. /// [Parameter] public bool SuppressInitialFocus { get; set; } diff --git a/src/EventLogExpert.Components/Modals/Alerts/AlertModal.razor.cs b/src/EventLogExpert.Components/Modals/Alerts/AlertModal.razor.cs index 3355d374..dc3bc786 100644 --- a/src/EventLogExpert.Components/Modals/Alerts/AlertModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Alerts/AlertModal.razor.cs @@ -7,7 +7,7 @@ namespace EventLogExpert.Components.Modals.Alerts; /// -/// Standalone alert modal used by ModalAlertDialogService when no host modal is active. Dismiss-only when +/// Standalone alert modal used by AlertDialogService when no host modal is active. Dismiss-only when /// is null; otherwise renders Accept/Cancel. /// public sealed partial class AlertModal : ModalBase diff --git a/src/EventLogExpert.Components/Modals/Alerts/PromptModal.razor.cs b/src/EventLogExpert.Components/Modals/Alerts/PromptModal.razor.cs index 04bbdc2d..68b5ff68 100644 --- a/src/EventLogExpert.Components/Modals/Alerts/PromptModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Alerts/PromptModal.razor.cs @@ -7,7 +7,7 @@ namespace EventLogExpert.Components.Modals.Alerts; /// -/// Standalone prompt modal used by ModalAlertDialogService when no host modal is active. Returns the input +/// Standalone prompt modal used by AlertDialogService when no host modal is active. Returns the input /// value on Accept, or on Cancel/Esc to match the existing /// IAlertDialogService.DisplayPrompt non-null contract. /// diff --git a/src/EventLogExpert.Components/Modals/DebugLogModal.razor b/src/EventLogExpert.Components/Modals/DebugLogModal.razor index 871b0ac7..6b64e033 100644 --- a/src/EventLogExpert.Components/Modals/DebugLogModal.razor +++ b/src/EventLogExpert.Components/Modals/DebugLogModal.razor @@ -1,6 +1,6 @@ -@inherits ModalBase @using EventLogExpert.UI @using Microsoft.Extensions.Logging +@inherits ModalBase + await FileSaveService.SaveAsync(suggestedFileName, FileSaveFileTypes.Log, async stream => { await using var writer = new StreamWriter(stream, leaveOpen: true); diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs index 7fe82e5e..9afa0f31 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs @@ -2,11 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.FilterCache; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterCache; +using EventLogExpert.UI.FilterPane; using Fluxor; using Microsoft.AspNetCore.Components; using System.Text.Json; @@ -34,7 +34,7 @@ protected override async Task OnExportAsync() { await FileSaveService.SaveAsync( "Saved Filters", - FileSaveServiceFileTypes.Json, + FileSaveFileTypes.Json, stream => JsonSerializer.SerializeAsync(stream, snapshot)); } catch (Exception ex) @@ -51,7 +51,7 @@ protected override async Task OnImportAsync() { var path = await FilePickerService.PickAsync( "Please select a json file to import", - FilePickerServiceFileTypes.Json); + FilePickerFileTypes.Json); if (path is null) { return; } @@ -60,7 +60,7 @@ protected override async Task OnImportAsync() if (filters is null) { return; } - Dispatcher.Dispatch(new FilterCacheAction.ImportFavorites(filters)); + Dispatcher.Dispatch(new ImportFavoritesAction(filters)); } catch (Exception ex) { @@ -70,11 +70,11 @@ await AlertDialogService.ShowAlert("Import Failed", } } - private void AddFavorite(string filter) => Dispatcher.Dispatch(new FilterCacheAction.AddFavoriteFilter(filter)); + private void AddFavorite(string filter) => Dispatcher.Dispatch(new AddFavoriteFilterAction(filter)); private async Task AddFilter(string filter) { - var model = FilterModel.TryCreate(filter, FilterType.Cached, isEnabled: true); + var model = SavedFilter.TryCreate(filter, FilterType.Cached, isEnabled: true); if (model is null) { @@ -85,11 +85,11 @@ await AlertDialogService.ShowAlert("Invalid Filter", return; } - Dispatcher.Dispatch(new FilterPaneAction.AddFilter(model)); + Dispatcher.Dispatch(new AddFilterAction(model)); await CloseAsync(); } private void RemoveFavorite(string filter) => - Dispatcher.Dispatch(new FilterCacheAction.RemoveFavoriteFilter(filter)); + Dispatcher.Dispatch(new RemoveFavoriteFilterAction(filter)); } diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs index 515d87ea..09cbd494 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs @@ -1,25 +1,27 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.FilterGroup; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterGroup; +using EventLogExpert.UI.FilterPane; using Microsoft.AspNetCore.Components; using System.Text.Json; using IDispatcher = Fluxor.IDispatcher; +using SetFilterAction = EventLogExpert.UI.FilterGroup.SetFilterAction; namespace EventLogExpert.Components.Modals.Filters; public sealed partial class FilterGroup { private readonly HashSet _editingFilters = []; - private readonly List _pendingDrafts = []; + private readonly List _pendingDrafts = []; private FilterGroupId? _trackedGroupId; - [Parameter] public FilterGroupModel Group { get; set; } = null!; + [Parameter] public SavedFilterGroup Group { get; set; } = null!; [Parameter] public FilterGroupModal Parent { get; set; } = null!; @@ -61,15 +63,15 @@ protected override void OnParametersSet() base.OnParametersSet(); } - private void AddFilter() => _pendingDrafts.Add(new FilterDraftModel { FilterType = FilterType.Advanced }); + private void AddFilter() => _pendingDrafts.Add(new FilterDraft { FilterType = FilterType.Advanced }); private async Task ApplyFilters() { - Dispatcher.Dispatch(new FilterPaneAction.ApplyFilterGroup(Group)); + Dispatcher.Dispatch(new ApplyFilterGroupAction(Group)); await Parent.CloseAsync(); } - private void CancelGroup() => Dispatcher.Dispatch(new FilterGroupAction.ToggleGroup(Group.Id)); + private void CancelGroup() => Dispatcher.Dispatch(new ToggleGroupAction(Group.Id)); private async Task CopyGroup() { @@ -90,7 +92,7 @@ private async Task ExportGroup() { await FileSaveService.SaveAsync( snapshot.DisplayName, - FileSaveServiceFileTypes.Json, + FileSaveFileTypes.Json, stream => JsonSerializer.SerializeAsync(stream, snapshot)); } catch (Exception ex) @@ -101,13 +103,13 @@ await AlertDialogService.ShowAlert("Export Failed", } } - private void HandlePendingDiscard(FilterDraftModel draft) => _pendingDrafts.Remove(draft); + private void HandlePendingDiscard(FilterDraft draft) => _pendingDrafts.Remove(draft); - private void HandlePendingSave(FilterDraftModel draft, FilterModel filter) + private void HandlePendingSave(FilterDraft draft, SavedFilter filter) { _pendingDrafts.Remove(draft); - Dispatcher.Dispatch(new FilterGroupAction.SetFilter(Group.Id, filter)); + Dispatcher.Dispatch(new SetFilterAction(Group.Id, filter)); } private async Task ImportGroup() @@ -116,12 +118,12 @@ private async Task ImportGroup() { var path = await FilePickerService.PickAsync( "Please select a json file to import", - FilePickerServiceFileTypes.Json); + FilePickerFileTypes.Json); if (path is null) { return; } await using var stream = File.OpenRead(path); - var group = await JsonSerializer.DeserializeAsync(stream); + var group = await JsonSerializer.DeserializeAsync(stream); if (group is null) { return; } @@ -131,7 +133,7 @@ private async Task ImportGroup() Filters = group.Filters }; - Dispatcher.Dispatch(new FilterGroupAction.SetGroup(updatedGroup)); + Dispatcher.Dispatch(new SetGroupAction(updatedGroup)); } catch (Exception ex) { @@ -153,7 +155,7 @@ private void OnRowEditingChanged((FilterId Id, bool IsEditing) change) } } - private void RemoveGroup() => Dispatcher.Dispatch(new FilterGroupAction.RemoveGroup(Group.Id)); + private void RemoveGroup() => Dispatcher.Dispatch(new RemoveGroupAction(Group.Id)); private async Task RenameGroup() { @@ -174,7 +176,7 @@ private async Task RenameGroup() return; } - Dispatcher.Dispatch(new FilterGroupAction.SetGroup(Group with { Name = newName })); + Dispatcher.Dispatch(new SetGroupAction(Group with { Name = newName })); } private void SaveGroup() @@ -184,8 +186,8 @@ private void SaveGroup() if (_pendingDrafts.Count > 0) { return; } - Dispatcher.Dispatch(new FilterGroupAction.SetGroup(Group)); + Dispatcher.Dispatch(new SetGroupAction(Group)); } - private void ToggleGroup() => Dispatcher.Dispatch(new FilterGroupAction.ToggleGroup(Group.Id)); + private void ToggleGroup() => Dispatcher.Dispatch(new ToggleGroupAction(Group.Id)); } diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor index a0a14a5a..514c471f 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor @@ -1,4 +1,3 @@ -@using EventLogExpert.UI.Models @inherits ModalBase JsonSerializer.SerializeAsync(stream, snapshot)); } catch (Exception ex) @@ -49,16 +50,16 @@ protected override async Task OnImportAsync() { var path = await FilePickerService.PickAsync( "Please select a json file to import", - FilePickerServiceFileTypes.Json); + FilePickerFileTypes.Json); if (path is null) { return; } await using var stream = File.OpenRead(path); - var groups = await JsonSerializer.DeserializeAsync>(stream); + var groups = await JsonSerializer.DeserializeAsync>(stream); if (groups is null) { return; } - Dispatcher.Dispatch(new FilterGroupAction.ImportGroups(groups)); + Dispatcher.Dispatch(new ImportGroupsAction(groups)); } catch (Exception ex) { @@ -69,5 +70,5 @@ await AlertDialogService.ShowAlert("Import Failed", } private void CreateGroup() => - Dispatcher.Dispatch(new FilterGroupAction.AddGroup(new FilterGroupModel { IsEditing = true })); + Dispatcher.Dispatch(new AddGroupAction(new SavedFilterGroup { IsEditing = true })); } diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor b/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor index 77f5c881..5a998e5d 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor @@ -1,5 +1,3 @@ -@using EventLogExpert.UI.Models -
diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor.cs index 743186ab..d3932b0d 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroupSection.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; diff --git a/src/EventLogExpert.Components/Modals/ModalHost.razor.cs b/src/EventLogExpert.Components/Modals/ModalHost.razor.cs index 7057790d..201f584c 100644 --- a/src/EventLogExpert.Components/Modals/ModalHost.razor.cs +++ b/src/EventLogExpert.Components/Modals/ModalHost.razor.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Modals; diff --git a/src/EventLogExpert.Components/Modals/ReleaseNotesModal.razor.cs b/src/EventLogExpert.Components/Modals/ReleaseNotesModal.razor.cs index cae76d53..8a97a78b 100644 --- a/src/EventLogExpert.Components/Modals/ReleaseNotesModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/ReleaseNotesModal.razor.cs @@ -2,7 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Common.Markdown; +using EventLogExpert.UI.Update.ReleaseNotes; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Modals; @@ -18,7 +19,7 @@ protected override void OnParametersSet() { // ReleaseNotesContent is a struct; defend against a missing parameter (default(struct)) // even though [EditorRequired] surfaces the omission as a build warning. - _html = ReleaseNotesMarkdownRenderer.RenderToHtml(Content.Title ?? string.Empty, Content.Markdown ?? string.Empty); + _html = MarkdownRenderer.RenderToHtml(Content.Title ?? string.Empty, Content.Markdown ?? string.Empty); base.OnParametersSet(); } diff --git a/src/EventLogExpert.Components/Modals/SettingsModal.razor b/src/EventLogExpert.Components/Modals/SettingsModal.razor index e7c5ddea..6ba76166 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor @@ -1,4 +1,5 @@ @using EventLogExpert.UI +@using EventLogExpert.UI.Common.Clipboard @using Microsoft.Extensions.Logging @inherits ModalBase @@ -84,10 +85,10 @@
Keyboard Copy Behavior: - - @foreach (CopyType item in Enum.GetValues(typeof(CopyType))) + + @foreach (EventCopyFormat item in Enum.GetValues(typeof(EventCopyFormat))) { - + }
diff --git a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index a5e0c9da..59e4a50b 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -2,10 +2,14 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.EventLog; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Banner; +using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Settings; using Fluxor; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; @@ -16,11 +20,11 @@ namespace EventLogExpert.Components.Modals; public sealed partial class SettingsModal : ModalBase { - private readonly Dictionary _pendingToggles = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _entriesUpgrading = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _pendingToggles = new(StringComparer.OrdinalIgnoreCase); private CancellationTokenSource? _classificationCts; - private CopyType _copyType; + private EventCopyFormat _copyFormat; private bool _databaseStateChanged; private volatile bool _disposed; private bool _isPreReleaseEnabled; @@ -41,14 +45,14 @@ public sealed partial class SettingsModal : ModalBase [Inject] private IFilePickerService FilePickerService { get; init; } = null!; - [Inject] private ILogReloadCoordinator LogReloadCoordinator { get; init; } = null!; + private bool IsAnyUpgradeInFlight => _entriesUpgrading.Count > 0 || BannerService.SettingsProgress is not null; private bool IsClassificationPending => !DatabaseService.InitialClassificationTask.IsCompleted; - private bool IsAnyUpgradeInFlight => _entriesUpgrading.Count > 0 || BannerService.SettingsProgress is not null; - private bool IsCloseBlocked => IsAnyUpgradeInFlight; + [Inject] private ILogReloadCoordinator LogReloadCoordinator { get; init; } = null!; + [Inject] private ISettingsService Settings { get; init; } = null!; protected override async ValueTask DisposeAsyncCore(bool disposing) @@ -56,7 +60,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) if (disposing) { _disposed = true; - _classificationCts?.Cancel(); + _classificationCts?.CancelAsync(); _classificationCts?.Dispose(); _classificationCts = null; DatabaseService.EntriesChanged -= OnDatabaseEntriesChanged; @@ -103,7 +107,7 @@ protected override async Task OnSaveAsync() await ApplyPendingToggles(); - Settings.CopyType = _copyType; + Settings.CopyFormat = _copyFormat; Settings.IsPreReleaseEnabled = _isPreReleaseEnabled; Settings.LogLevel = _logLevel; Settings.ShowDisplayPaneOnSelectionChange = _showDisplayPaneOnSelectionChange; @@ -198,7 +202,7 @@ private async Task ImportDatabase() { var sourcePaths = await FilePickerService.PickMultipleAsync( "Please select database files to import", - FilePickerServiceFileTypes.Database); + FilePickerFileTypes.Database); if (sourcePaths.Count == 0) { return; } @@ -224,7 +228,7 @@ await AlertDialogService.ShowAlert("Import Failed", private void LoadFromSettings() { - _copyType = Settings.CopyType; + _copyFormat = Settings.CopyFormat; _isPreReleaseEnabled = Settings.IsPreReleaseEnabled; _logLevel = Settings.LogLevel; _showDisplayPaneOnSelectionChange = Settings.ShowDisplayPaneOnSelectionChange; @@ -281,11 +285,11 @@ private async Task ReloadOpenLogs() var logsToReopen = EventLogState.Value.ActiveLogs.Values; - Dispatcher.Dispatch(new EventLogAction.CloseAll()); + Dispatcher.Dispatch(new CloseAllAction()); foreach (var log in logsToReopen) { - Dispatcher.Dispatch(new EventLogAction.OpenLog(log.Name, log.Type)); + Dispatcher.Dispatch(new OpenLogAction(log.Name, log.Type)); } } diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 5ab39b3e..93306bc0 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -9,7 +9,24 @@ @using EventLogExpert.Components.Menu @using EventLogExpert.Components.Modals @using EventLogExpert.Components.Modals.Alerts -@using EventLogExpert.UI.Models -@using EventLogExpert.UI.Services +@using EventLogExpert.UI.Common.Display +@using EventLogExpert.UI.Common.Files +@using EventLogExpert.UI.Banner +@using EventLogExpert.UI.Alerts +@using EventLogExpert.UI.Settings +@using EventLogExpert.UI.Menu +@using EventLogExpert.UI.Database +@using EventLogExpert.UI.Database.Upgrade +@using EventLogExpert.UI.Update @using Fluxor @using Fluxor.Blazor.Web.Components + +@using EventLogExpert.UI.LogTable +@using EventLogExpert.UI.EventLog +@using EventLogExpert.UI.Filter + +@using EventLogExpert.UI.FilterCache + +@using EventLogExpert.UI.FilterGroup + +@using EventLogExpert.UI.FilterPane diff --git a/src/EventLogExpert.EventDbTool/MtaProviderSource.cs b/src/EventLogExpert.EventDbTool/MtaProviderSource.cs index 994deb91..10e81957 100644 --- a/src/EventLogExpert.EventDbTool/MtaProviderSource.cs +++ b/src/EventLogExpert.EventDbTool/MtaProviderSource.cs @@ -10,15 +10,15 @@ namespace EventLogExpert.EventDbTool; /// -/// Loads from an exported .evtx log paired with its sibling -/// LocaleMetaData/*.MTA files. Provider names are discovered by reading the .evtx events, -/// and the MTA files are used as the only metadata source (no registry/DLL fallback). +/// Loads from an exported .evtx log paired with its sibling LocaleMetaData/*.MTA +/// files. Provider names are discovered by reading the .evtx events, and the MTA files are used as the only metadata +/// source (no registry/DLL fallback). /// internal static class MtaProviderSource { /// - /// Reads and returns the distinct provider names referenced by its - /// event records. Does NOT require sibling LocaleMetaData/*.MTA files. + /// Reads and returns the distinct provider names referenced by its event records. + /// Does NOT require sibling LocaleMetaData/*.MTA files. /// public static IReadOnlyList DiscoverProviderNames( string evtxPath, @@ -27,9 +27,8 @@ public static IReadOnlyList DiscoverProviderNames( !RegexHelper.TryCreate(filter, logger, out var regex) ? [] : DiscoverProviderNamesCore(evtxPath, logger, regex); /// - /// Returns the sibling LocaleMetaData/*.MTA files for , or an empty - /// array if none are present. Logs an error in the empty case to surface the misconfiguration - /// without throwing. + /// Returns the sibling LocaleMetaData/*.MTA files for , or an empty array if none are + /// present. Logs an error in the empty case to surface the misconfiguration without throwing. /// public static IReadOnlyList FindMtaFiles(string evtxPath, ITraceLogger logger) { @@ -81,11 +80,10 @@ public static IReadOnlyList FindMtaFiles(string evtxPath, ITraceLogger l } /// - /// Discovers provider names from and yields a - /// for each that can be resolved exclusively from the sibling - /// LocaleMetaData/*.MTA files. Providers that cannot be resolved from any MTA file are - /// skipped with a warning so callers never persist empty placeholder providers (which would - /// defeat the "no local fallback" guarantee when the resulting database is consumed later). + /// Discovers provider names from and yields a for + /// each that can be resolved exclusively from the sibling LocaleMetaData/*.MTA files. Providers that cannot be + /// resolved from any MTA file are skipped with a warning so callers never persist empty placeholder providers (which + /// would defeat the "no local fallback" guarantee when the resulting database is consumed later). /// public static IEnumerable LoadProviders( string evtxPath, @@ -95,8 +93,8 @@ public static IEnumerable LoadProviders( LoadProvidersCore(evtxPath, logger, regex, null, null); /// - /// Internal overload used by so name-based filtering and the de-dup - /// seen set are applied BEFORE the expensive MTA resolution per provider. + /// Internal overload used by so name-based filtering and the de-dup seen set + /// are applied BEFORE the expensive MTA resolution per provider. /// internal static IEnumerable LoadProviders( string evtxPath, diff --git a/src/EventLogExpert.EventDbTool/ProviderSource.cs b/src/EventLogExpert.EventDbTool/ProviderSource.cs index c076ba63..79f59597 100644 --- a/src/EventLogExpert.EventDbTool/ProviderSource.cs +++ b/src/EventLogExpert.EventDbTool/ProviderSource.cs @@ -12,16 +12,16 @@ namespace EventLogExpert.EventDbTool; /// -/// Loads from a path that may be a .db file, an exported .evtx file, or a -/// folder. When the path is a folder, all top-level *.db files are processed first (sorted), followed by -/// all top-level *.evtx files (sorted). Subdirectories are not searched. +/// Loads from a path that may be a .db file, an exported .evtx file, or a folder. +/// When the path is a folder, all top-level *.db files are processed first (sorted), followed by all top-level *.evtx +/// files (sorted). Subdirectories are not searched. /// internal static class ProviderSource { /// - /// Conservative cap on the number of parameters in a single Where(... Contains) SQL IN - /// clause. SQLite's default limit is 999 parameters; we stay well under that so the same code - /// works on older SQLite builds too. Larger requests are split into multiple round-trips. + /// Conservative cap on the number of parameters in a single Where(... Contains) SQL IN clause. SQLite's + /// default limit is 999 parameters; we stay well under that so the same code works on older SQLite builds too. Larger + /// requests are split into multiple round-trips. /// internal const int MaxInClauseParameters = 500; @@ -35,7 +35,7 @@ internal static class ProviderSource public static IReadOnlyList LoadProviderNames(string path, ITraceLogger logger, string? filter = null) => !RegexHelper.TryCreate(filter, logger, out var regex) ? [] : LoadProviderNames(path, logger, regex); - /// + /// public static IReadOnlyList LoadProviderNames(string path, ITraceLogger logger, Regex? regex) { var names = new SortedSet(StringComparer.OrdinalIgnoreCase); @@ -52,11 +52,11 @@ public static IReadOnlyList LoadProviderNames(string path, ITraceLogger } /// - /// Loads from , applying an optional - /// case-insensitive regex to provider names. When the same provider name - /// appears in multiple source files, the first occurrence wins (.db files are processed before .evtx). - /// Provider names contained in are excluded BEFORE details are - /// resolved, so callers using the skip set never pay the cost of loading metadata for excluded providers. + /// Loads from , applying an optional case-insensitive + /// regex to provider names. When the same provider name appears in multiple source files, + /// the first occurrence wins (.db files are processed before .evtx). Provider names contained in + /// are excluded BEFORE details are resolved, so callers using the skip set never + /// pay the cost of loading metadata for excluded providers. /// public static IEnumerable LoadProviders( string path, @@ -66,7 +66,7 @@ public static IEnumerable LoadProviders( !RegexHelper.TryCreate(filter, logger, out var regex) ? [] : LoadProvidersIterator(path, logger, regex, skipProviderNames); - /// + /// public static IEnumerable LoadProviders( string path, ITraceLogger logger, @@ -143,9 +143,8 @@ public static bool ValidateSourceSchemas(string path, ITraceLogger logger) } /// - /// Expands into the ordered list of source files: a single .db or .evtx - /// when given a file; or all *.db files (sorted) followed by all *.evtx files (sorted) when given a - /// folder. + /// Expands into the ordered list of source files: a single .db or .evtx when given a + /// file; or all *.db files (sorted) followed by all *.evtx files (sorted) when given a folder. /// private static IEnumerable EnumerateSourceFiles(string path, ITraceLogger logger) { @@ -176,14 +175,14 @@ private static IEnumerable EnumerateSourceFiles(string path, ITraceLogge // way TryValidate accepts them. Files are bucketed (.db first, then .evtx) and sorted // OrdinalIgnoreCase within each bucket so first-occurrence-wins ordering is stable. var dbFiles = allFiles - .Where(f => string.Equals(System.IO.Path.GetExtension(f), DbExtension, StringComparison.OrdinalIgnoreCase)) + .Where(f => string.Equals(Path.GetExtension(f), DbExtension, StringComparison.OrdinalIgnoreCase)) .ToArray(); Array.Sort(dbFiles, StringComparer.OrdinalIgnoreCase); foreach (var f in dbFiles) { yield return f; } var evtxFiles = allFiles - .Where(f => string.Equals(System.IO.Path.GetExtension(f), EvtxExtension, StringComparison.OrdinalIgnoreCase)) + .Where(f => string.Equals(Path.GetExtension(f), EvtxExtension, StringComparison.OrdinalIgnoreCase)) .ToArray(); Array.Sort(evtxFiles, StringComparer.OrdinalIgnoreCase); diff --git a/src/EventLogExpert.EventDbTool/RegexHelper.cs b/src/EventLogExpert.EventDbTool/RegexHelper.cs index f534fbbb..b7e445fc 100644 --- a/src/EventLogExpert.EventDbTool/RegexHelper.cs +++ b/src/EventLogExpert.EventDbTool/RegexHelper.cs @@ -7,10 +7,10 @@ namespace EventLogExpert.EventDbTool; /// -/// Centralized creation of instances for user-supplied --filter patterns. -/// Always sets a match timeout to bound worst-case execution against catastrophic backtracking, and -/// converts from invalid patterns into a logged error rather than -/// letting it terminate the process. +/// Centralized creation of instances for user-supplied --filter patterns. Always sets +/// a match timeout to bound worst-case execution against catastrophic backtracking, and converts +/// from invalid patterns into a logged error rather than letting it terminate the +/// process. /// internal static class RegexHelper { @@ -18,10 +18,10 @@ internal static class RegexHelper private static readonly TimeSpan s_matchTimeout = TimeSpan.FromSeconds(1); /// - /// Attempts to compile into a case-insensitive with - /// a bounded match timeout. A null/empty pattern is treated as "no filter": - /// is set to and the method still returns so callers - /// can distinguish an absent filter from a malformed one. + /// Attempts to compile into a case-insensitive with a bounded + /// match timeout. A null/empty pattern is treated as "no filter": is set to + /// and the method still returns so callers can distinguish an absent + /// filter from a malformed one. /// public static bool TryCreate(string? pattern, ITraceLogger logger, out Regex? regex) { diff --git a/src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs b/src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs index d05b7a0c..b68a0e81 100644 --- a/src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs +++ b/src/EventLogExpert.Eventing/Common/Channels/LogChannelMethods.cs @@ -7,7 +7,10 @@ public static class LogChannelMethods { private const string MicrosoftWindowsPrefix = "Microsoft-Windows-"; - /// Live event log channels hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid duplicates. + /// + /// Live event log channels hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid + /// duplicates. + /// public static IReadOnlySet HardCodedLiveChannels { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) { LogChannelNames.ApplicationLog, diff --git a/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs b/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs index 1da19cd0..88415bf6 100644 --- a/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs +++ b/src/EventLogExpert.Eventing/Common/Databases/DatabasePathSorter.cs @@ -4,22 +4,20 @@ namespace EventLogExpert.Eventing.Common.Databases; /// -/// Sorts provider-database identifiers (full paths or bare file names) into resolver -/// load-priority order, so that load-priority cascading works the same regardless of -/// which layer is invoking it. +/// Sorts provider-database identifiers (full paths or bare file names) into resolver load-priority order, so that +/// load-priority cascading works the same regardless of which layer is invoking it. /// /// /// -/// Order is: ascending by product name, then descending by version (numeric where the -/// version token parses as an integer so "10" sorts after "2"), then descending by raw -/// version string for tie-breaks. Inputs whose extensionless file name does not end in -/// a space-delimited version token are sorted by their full extensionless name with -/// no version key. +/// Order is: ascending by product name, then descending by version (numeric where the version token parses as an +/// integer so "10" sorts after "2"), then descending by raw version string for tie-breaks. Inputs whose +/// extensionless file name does not end in a space-delimited version token are sorted by their full extensionless +/// name with no version key. /// /// -/// Returned strings are the originals — directory and extension are used only for key -/// extraction, never for reconstruction. A caller passing full paths gets full paths -/// back; a caller passing bare file names gets bare file names back. +/// Returned strings are the originals — directory and extension are used only for key extraction, never for +/// reconstruction. A caller passing full paths gets full paths back; a caller passing bare file names gets bare +/// file names back. /// /// public static class DatabasePathSorter diff --git a/src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs b/src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs index a71f03f1..e6eddac8 100644 --- a/src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs +++ b/src/EventLogExpert.Eventing/Common/Events/ResolvedEvent.cs @@ -42,10 +42,10 @@ public sealed record ResolvedEvent( public SecurityIdentifier? UserId { get; init; } /// - /// Pre-rendered XML for the event. Populated by EventLogReader only when the - /// log is opened with renderXml: true (currently driven by the presence of an - /// applied filter that references this property). When empty, callers should use - /// to fetch the XML on demand. + /// Pre-rendered XML for the event. Populated by EventLogReader only when the log is opened with + /// renderXml: true (currently driven by the presence of an applied filter that references this property). When + /// empty, callers should use to fetch the XML on + /// demand. /// public string Xml { get; init; } = string.Empty; } diff --git a/src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs b/src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs index 6a1b6240..844aaff1 100644 --- a/src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeErrorResolver.cs @@ -13,10 +13,9 @@ internal static class NativeErrorResolver private static ConcurrentDictionary s_ntStatusCache = new(); /// - /// Resolves an HRESULT or Win32 error code to a human-readable string. - /// Uses the system message table via FormatMessage, falling back to ntdll.dll's message table - /// for codes not found in the system table (e.g., NTSTATUS codes). - /// Results are cached to avoid repeated P/Invoke calls. + /// Resolves an HRESULT or Win32 error code to a human-readable string. Uses the system message table via + /// FormatMessage, falling back to ntdll.dll's message table for codes not found in the system table (e.g., NTSTATUS + /// codes). Results are cached to avoid repeated P/Invoke calls. /// internal static string GetErrorMessage(uint hResult) => GetOrAddBounded(ref s_hResultCache, hResult, static code => @@ -32,10 +31,9 @@ internal static string GetNtStatusMessage(uint ntStatus) => $"0x{status:X8}"); /// - /// Bounded cache lookup with atomic swap eviction. On a cache hit the entry is returned - /// immediately regardless of cache size. On a miss, if the cache has reached - /// the entire dictionary is atomically swapped with a fresh - /// instance (only one thread performs the swap) before inserting the new entry. + /// Bounded cache lookup with atomic swap eviction. On a cache hit the entry is returned immediately regardless of + /// cache size. On a miss, if the cache has reached the entire dictionary is atomically + /// swapped with a fresh instance (only one thread performs the swap) before inserting the new entry. /// private static string GetOrAddBounded( ref ConcurrentDictionary cache, diff --git a/src/EventLogExpert.Eventing/Interop/NativeMethods.cs b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs index 7fdb924a..ddcde71c 100644 --- a/src/EventLogExpert.Eventing/Interop/NativeMethods.cs +++ b/src/EventLogExpert.Eventing/Interop/NativeMethods.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using System.Buffers; using System.Runtime.InteropServices; // ReSharper disable InconsistentNaming @@ -20,10 +21,9 @@ internal static partial class NativeMethods private const string Kernel32Api = "kernel32.dll"; /// - /// Detects unresolved FormatMessage insert placeholders in the result. Catches positional - /// inserts (%1 through %99, including forms like %1!s!) and common printf-style specifiers - /// (%s, %d, %p, etc.) that appear in some message tables. Skips escaped percent signs (%%). - /// Does not detect compound printf forms like %lu or %I64u. + /// Detects unresolved FormatMessage insert placeholders in the result. Catches positional inserts (%1 through + /// %99, including forms like %1!s!) and common printf-style specifiers (%s, %d, %p, etc.) that appear in some message + /// tables. Skips escaped percent signs (%%). Does not detect compound printf forms like %lu or %I64u. /// internal static bool ContainsFormatInsert(ReadOnlySpan text) { @@ -136,7 +136,7 @@ internal static partial LibraryHandle LoadLibraryExW( foreach (int size in retrySizes) { - char[] rented = System.Buffers.ArrayPool.Shared.Rent(size); + char[] rented = ArrayPool.Shared.Rent(size); try { @@ -148,7 +148,7 @@ internal static partial LibraryHandle LoadLibraryExW( } finally { - System.Buffers.ArrayPool.Shared.Return(rented); + ArrayPool.Shared.Return(rented); } } diff --git a/src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs b/src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs index c01d9b02..527398c0 100644 --- a/src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs +++ b/src/EventLogExpert.Eventing/Interop/Win32ErrorCodes.cs @@ -6,26 +6,26 @@ namespace EventLogExpert.Eventing.Interop; internal static class Win32ErrorCodes { - internal const int ERROR_FILE_NOT_FOUND = 2; - internal const int ERROR_PATH_NOT_FOUND = 3; internal const int ERROR_ACCESS_DENIED = 5; - internal const int ERROR_INVALID_HANDLE = 6; - internal const int ERROR_INVALID_DATA = 13; - internal const int ERROR_INVALID_PARAMETER = 87; - - internal const int ERROR_INSUFFICIENT_BUFFER = 122; - internal const int ERROR_NO_MORE_ITEMS = 259; - - internal const int RPC_S_CALL_CANCELED = 1818; internal const int ERROR_CANCELLED = 1223; - - internal const int ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND = 0x3A9A; - internal const int ERROR_EVT_INVALID_EVENT_DATA = 0x3A9D; internal const int ERROR_EVT_CHANNEL_NOT_FOUND = 0x3A9F; + internal const int ERROR_EVT_INVALID_EVENT_DATA = 0x3A9D; + internal const int ERROR_EVT_MAX_INSERTS_REACHED = 0x3AB7; + internal const int ERROR_EVT_MESSAGE_ID_NOT_FOUND = 0x3AB4; internal const int ERROR_EVT_MESSAGE_NOT_FOUND = 0x3AB3; - internal const int ERROR_EVT_MESSAGE_ID_NOT_FOUND = 0x3AB4; - internal const int ERROR_EVT_UNRESOLVED_VALUE_INSERT = 0x3AB5; + + internal const int ERROR_EVT_PUBLISHER_METADATA_NOT_FOUND = 0x3A9A; internal const int ERROR_EVT_UNRESOLVED_PARAMETER_INSERT = 0x3AB6; - internal const int ERROR_EVT_MAX_INSERTS_REACHED = 0x3AB7; + internal const int ERROR_EVT_UNRESOLVED_VALUE_INSERT = 0x3AB5; + internal const int ERROR_FILE_NOT_FOUND = 2; + + internal const int ERROR_INSUFFICIENT_BUFFER = 122; + internal const int ERROR_INVALID_DATA = 13; + internal const int ERROR_INVALID_HANDLE = 6; + internal const int ERROR_INVALID_PARAMETER = 87; + internal const int ERROR_NO_MORE_ITEMS = 259; + internal const int ERROR_PATH_NOT_FOUND = 3; + + internal const int RPC_S_CALL_CANCELED = 1818; } diff --git a/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs b/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs index fd6c2036..733dd738 100644 --- a/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs +++ b/src/EventLogExpert.Eventing/Providers/ProviderDetails.cs @@ -62,9 +62,10 @@ internal IReadOnlyList GetEventsById(long id) return _eventsByIdLookup.TryGetValue(id, out var list) ? list : []; } - /// Gets messages matching the given ShortId using a pre-built lookup dictionary. - /// The lookup uses int keys derived from unsigned reinterpretation of ShortId, - /// matching the implicit ushort-to-int promotion used by callers. + /// + /// Gets messages matching the given ShortId using a pre-built lookup dictionary. The lookup uses int keys derived + /// from unsigned reinterpretation of ShortId, matching the implicit ushort-to-int promotion used by callers. + /// internal IReadOnlyList GetMessagesByShortId(int shortId) { _messagesByShortIdLookup ??= BuildMessagesByShortIdLookup(); diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index eb961f90..d0e612bc 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -13,10 +13,9 @@ internal sealed class RegistryProvider(ITraceLogger? logger = null) /// Returns the file paths for the message files for this provider on the local machine. /// - /// EventLogExpert is a local-only tool. Remote-machine resolution is intentionally not - /// supported because the modern provider metadata path (used as a fallback when no legacy - /// registry entry exists) is local-only, and silently mixing local and remote sources - /// produced wrong message text. Callers must already be operating in a local context. + /// EventLogExpert is a local-only tool. Remote-machine resolution is intentionally not supported because the + /// modern provider metadata path (used as a fallback when no legacy registry entry exists) is local-only, and silently + /// mixing local and remote sources produced wrong message text. Callers must already be operating in a local context. /// public IReadOnlyList GetMessageFilesForLegacyProvider(string providerName) { diff --git a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs index 8a954912..8e8adbda 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogReader.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogReader.cs @@ -17,19 +17,18 @@ public sealed partial class EventLogReader(string path, LogPathType pathType, bo private int _disposed; /// - /// when the underlying EvtQuery handle was opened successfully. - /// When , the path could not be opened (invalid log name, missing or - /// corrupt .evtx file, access denied, etc.) and will not return events. + /// when the underlying EvtQuery handle was opened successfully. When + /// , the path could not be opened (invalid log name, missing or corrupt .evtx file, access + /// denied, etc.) and will not return events. /// public bool IsValid => _handle is { IsInvalid: false }; public string? LastBookmark { get; private set; } /// - /// When returns due to a Win32 error other - /// than ERROR_NO_MORE_ITEMS, this property contains the Win32 error code. A value of - /// means either no error occurred or the last failure was a normal - /// end-of-results condition. + /// When returns due to a Win32 error other than + /// ERROR_NO_MORE_ITEMS, this property contains the Win32 error code. A value of means + /// either no error occurred or the last failure was a normal end-of-results condition. /// public int? LastErrorCode { get; private set; } diff --git a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs index 66feb96a..1339054b 100644 --- a/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs +++ b/src/EventLogExpert.Eventing/Readers/EventLogWatcher.cs @@ -52,12 +52,10 @@ public EventLogWatcher(string path, bool renderXml = false) : this(path, null, r /// Gets or sets whether the watcher is actively subscribed. /// - /// Thrown when set to false from inside an handler - /// while the watcher is still subscribed. - /// - /// - /// Thrown when set to true after the watcher has been disposed. + /// Thrown when set to false from inside an + /// handler while the watcher is still subscribed. /// + /// Thrown when set to true after the watcher has been disposed. public bool Enabled { get @@ -81,8 +79,8 @@ public bool Enabled /// Releases the native subscription and query handles; idempotent. /// - /// Thrown when invoked from inside an handler unless - /// the watcher has already been disposed. + /// Thrown when invoked from inside an + /// handler unless the watcher has already been disposed. /// public void Dispose() { diff --git a/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs index 95b9190a..4f30c6b0 100644 --- a/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolver.cs @@ -14,10 +14,8 @@ namespace EventLogExpert.Eventing.Resolvers; /// -/// Unified event resolver that implements a fallback chain: -/// 1. MTA locale metadata files (primary, for exported logs) -/// 2. SQLite provider databases (secondary) -/// 3. Local provider registry (fallback) +/// Unified event resolver that implements a fallback chain: 1. MTA locale metadata files (primary, for exported +/// logs) 2. SQLite provider databases (secondary) 3. Local provider registry (fallback) /// public sealed class EventResolver : EventResolverBase, IEventResolver { diff --git a/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs b/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs index b34b3034..4d419abb 100644 --- a/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs +++ b/src/EventLogExpert.Eventing/Resolvers/EventResolverBase.cs @@ -144,9 +144,8 @@ protected virtual void Dispose(bool disposing) } /// - /// Override in derived classes to provide a supplemental ProviderDetails - /// when the primary source (MTA/DB) has partial coverage for a provider. - /// Called only when the primary provider exists but couldn't match the event. + /// Override in derived classes to provide a supplemental ProviderDetails when the primary source (MTA/DB) has + /// partial coverage for a provider. Called only when the primary provider exists but couldn't match the event. /// protected virtual ProviderDetails? TryGetSupplementalDetails(EventRecord eventRecord) => null; @@ -249,9 +248,8 @@ private static void ResizeBuffer(ref char[] buffer, ref Span source, int s } /// - /// Disambiguate multiple legacy messages for the same event ID. Tries Qualifier match - /// (high 16 bits of RawId), then LogLink, then severity. Returns null when the set is - /// empty or remains ambiguous after all checks. + /// Disambiguate multiple legacy messages for the same event ID. Tries Qualifier match (high 16 bits of RawId), + /// then LogLink, then severity. Returns null when the set is empty or remains ambiguous after all checks. /// private static MessageModel? TryDisambiguateLegacyMessage(EventRecord eventRecord, IReadOnlyList legacyMessages) { @@ -374,10 +372,9 @@ private string CacheTaskName(string taskName) } /// - /// Counts the number of "visible" template properties by excluding length-prefixed - /// binary data length fields. When a <data> element has a length attribute - /// referencing another <data> element's name, Windows consumes the referenced - /// length field internally and does not surface it as a user property via EvtRender. + /// Counts the number of "visible" template properties by excluding length-prefixed binary data length fields. + /// When a <data> element has a length attribute referencing another <data> element's name, + /// Windows consumes the referenced length field internally and does not surface it as a user property via EvtRender. /// private int CountVisibleTemplateProperties(ReadOnlySpan template) { @@ -494,10 +491,9 @@ private ResolvedEvent CreateEventModel( }; /// - /// Relaxed template match for exact Id+Version+LogName matches only. - /// Allows the template to have exactly 1 more data node than the event - /// has properties, which handles version mismatches where the manifest - /// added an optional field in a newer version. + /// Relaxed template match for exact Id+Version+LogName matches only. Allows the template to have exactly 1 more + /// data node than the event has properties, which handles version mismatches where the manifest added an optional + /// field in a newer version. /// private bool DoesTemplateApproximatelyMatchPropertyCount(ReadOnlySpan template, int eventPropertyCount) { @@ -1107,10 +1103,9 @@ private string[] GetOrParseTemplateDataNodes(ReadOnlySpan template) } /// - /// Picks the parameter table for %%n substitutions, biased toward whichever provider - /// supplied the description text. When - /// is true, prefer supplemental's parameters and fall back to primary; otherwise prefer - /// primary and fall back to supplemental (lazily loading it when not yet available). + /// Picks the parameter table for %%n substitutions, biased toward whichever provider supplied the description + /// text. When is true, prefer supplemental's parameters and fall back + /// to primary; otherwise prefer primary and fall back to supplemental (lazily loading it when not yet available). /// private IEnumerable GetParametersForDescription( ProviderDetails? primary, @@ -1134,9 +1129,9 @@ private IEnumerable GetParametersForDescription( } /// - /// Returns the outType values for only visible template properties, excluding hidden - /// length-provider fields that Windows consumes internally. This ensures the outType - /// array aligns correctly with the properties surfaced by EvtRender. + /// Returns the outType values for only visible template properties, excluding hidden length-provider fields that + /// Windows consumes internally. This ensures the outType array aligns correctly with the properties surfaced by + /// EvtRender. /// private string[] GetVisibleTemplateOutTypes(ReadOnlySpan template) { diff --git a/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs b/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs index 6eaf7408..bf0ec347 100644 --- a/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs +++ b/src/EventLogExpert.Eventing/Resolvers/IEventXmlResolver.cs @@ -6,9 +6,9 @@ namespace EventLogExpert.Eventing.Resolvers; /// -/// Resolves the raw XML for a on demand and caches the result. Implementations -/// must be thread-safe and must coalesce concurrent requests for the same event into a single underlying -/// EvtQuery / RenderEventXml call. +/// Resolves the raw XML for a on demand and caches the result. Implementations must +/// be thread-safe and must coalesce concurrent requests for the same event into a single underlying EvtQuery / +/// RenderEventXml call. /// public interface IEventXmlResolver { @@ -19,10 +19,10 @@ public interface IEventXmlResolver void ClearXmlCacheForLog(string owningLog); /// - /// Returns the XML for . If is already populated - /// (because the log was opened with renderXml: true), the pre-rendered value is returned immediately; otherwise - /// the resolver re-opens the source log via EvtQuery, locates the record by - /// , and renders the XML. + /// Returns the XML for . If is already populated (because + /// the log was opened with renderXml: true), the pre-rendered value is returned immediately; otherwise the + /// resolver re-opens the source log via EvtQuery, locates the record by , + /// and renders the XML. /// /// The event to resolve XML for. /// diff --git a/src/EventLogExpert.UI/Services/ModalAlertDialogService.cs b/src/EventLogExpert.UI/Alerts/AlertDialogService.cs similarity index 92% rename from src/EventLogExpert.UI/Services/ModalAlertDialogService.cs rename to src/EventLogExpert.UI/Alerts/AlertDialogService.cs index f1a8b228..da7f4a35 100644 --- a/src/EventLogExpert.UI/Services/ModalAlertDialogService.cs +++ b/src/EventLogExpert.UI/Alerts/AlertDialogService.cs @@ -1,21 +1,21 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Banner; +using EventLogExpert.UI.Common.Threading; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Alerts; -public sealed class ModalAlertDialogService( - IModalService modalService, +public sealed class AlertDialogService( + IInlineAlertHostBroker inlineAlertHostBroker, IMainThreadService mainThreadService, IBannerService bannerService, Func, Task> openStandaloneAlert, Func, Task> openStandalonePrompt) : IAlertDialogService { private readonly IBannerService _bannerService = bannerService; + private readonly IInlineAlertHostBroker _inlineAlertHostBroker = inlineAlertHostBroker; private readonly IMainThreadService _mainThreadService = mainThreadService; - private readonly IModalService _modalService = modalService; private readonly Func, Task> _openStandaloneAlert = openStandaloneAlert; private readonly Func, Task> _openStandalonePrompt = openStandalonePrompt; @@ -62,7 +62,7 @@ public Task ShowErrorAlert(string title, string message, string? actionLabel = n private Task DisplayPromptCore(string title, string message, string? initialValue) => InvokeOnMainThreadAsync(async () => { - if (!_modalService.TryGetActiveAlertHost(out var host)) + if (!_inlineAlertHostBroker.TryGet(out var host)) { return await _openStandalonePrompt(new Dictionary { @@ -111,7 +111,7 @@ private Task ShowAlertCore( return InvokeOnMainThreadAsync(async () => { - bool hostAvailable = _modalService.TryGetActiveAlertHost(out var host); + bool hostAvailable = _inlineAlertHostBroker.TryGet(out var host); if (presentation == AlertPresentation.InlineOnly && !hostAvailable) { diff --git a/src/EventLogExpert.UI/Alerts/AlertPresentation.cs b/src/EventLogExpert.UI/Alerts/AlertPresentation.cs new file mode 100644 index 00000000..e9b4777a --- /dev/null +++ b/src/EventLogExpert.UI/Alerts/AlertPresentation.cs @@ -0,0 +1,32 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Banner; + +namespace EventLogExpert.UI.Alerts; + +/// +/// Controls how an request +/// is surfaced to the user. +/// +public enum AlertPresentation +{ + /// + /// Default. Use the existing routing: render inline in the active modal host if + /// one is registered, otherwise open a standalone alert popup. + /// + Auto, + + /// + /// Route to with severity. + /// Only valid for one-button overloads (the banner has no accept/cancel pair); using it on a two-button overload + /// throws. + /// + Banner, + + /// Require an active inline alert host. Throws if none is registered. + InlineOnly, + + /// Always open a standalone popup, even if an inline host is registered. + PopupOnly, +} diff --git a/src/EventLogExpert.UI/Services/EmptyLogAlertFormatter.cs b/src/EventLogExpert.UI/Alerts/EmptyLogAlertFormatter.cs similarity index 84% rename from src/EventLogExpert.UI/Services/EmptyLogAlertFormatter.cs rename to src/EventLogExpert.UI/Alerts/EmptyLogAlertFormatter.cs index 73d293f7..718ef4fd 100644 --- a/src/EventLogExpert.UI/Services/EmptyLogAlertFormatter.cs +++ b/src/EventLogExpert.UI/Alerts/EmptyLogAlertFormatter.cs @@ -1,11 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Alerts; /// -/// Builds the message text shown when one or more logs the user just tried to open contained zero -/// events. Singular and plural cases use distinct phrasings. +/// Builds the message text shown when one or more logs the user just tried to open contained zero events. +/// Singular and plural cases use distinct phrasings. /// public static class EmptyLogAlertFormatter { diff --git a/src/EventLogExpert.UI/Interfaces/IAlertDialogService.cs b/src/EventLogExpert.UI/Alerts/IAlertDialogService.cs similarity index 90% rename from src/EventLogExpert.UI/Interfaces/IAlertDialogService.cs rename to src/EventLogExpert.UI/Alerts/IAlertDialogService.cs index b2650f1d..5a035847 100644 --- a/src/EventLogExpert.UI/Interfaces/IAlertDialogService.cs +++ b/src/EventLogExpert.UI/Alerts/IAlertDialogService.cs @@ -1,9 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; - -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Alerts; public interface IAlertDialogService { diff --git a/src/EventLogExpert.UI/Alerts/IInlineAlertHost.cs b/src/EventLogExpert.UI/Alerts/IInlineAlertHost.cs new file mode 100644 index 00000000..c57c8444 --- /dev/null +++ b/src/EventLogExpert.UI/Alerts/IInlineAlertHost.cs @@ -0,0 +1,14 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Alerts; + +/// +/// Implemented by an active modal so alerts can be routed as inline banners instead of opening a separate alert +/// modal (which would cancel the active one). +/// +public interface IInlineAlertHost +{ + /// Show inline. Replaces any prior pending inline alert (its task is canceled). + Task ShowInlineAlertAsync(InlineAlertRequest request, CancellationToken cancellationToken); +} diff --git a/src/EventLogExpert.UI/Alerts/IInlineAlertHostBroker.cs b/src/EventLogExpert.UI/Alerts/IInlineAlertHostBroker.cs new file mode 100644 index 00000000..c725dd89 --- /dev/null +++ b/src/EventLogExpert.UI/Alerts/IInlineAlertHostBroker.cs @@ -0,0 +1,30 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Alerts; + +/// +/// Tracks the currently registered so cross-cutting alert dispatchers can route +/// inline alerts to the active modal without forcing the Modal/ slice to know about the Alerts/ slice. Stale +/// registrations (from modals that have already been replaced) are rejected so a torn-down host never receives alerts. +/// +public interface IInlineAlertHostBroker +{ + /// + /// Register as the active inline-alert host for the modal identified by + /// . Stale ids (from replaced modals) are ignored. + /// + void Register(long modalId, IInlineAlertHost host); + + /// + /// Returns the currently registered inline-alert host, if any. Inspect on every alert because the active modal + /// may have changed since the last call. + /// + bool TryGet(out IInlineAlertHost? host); + + /// + /// Clear the active inline-alert host. Stale ids (from modals that no longer own the registration) are ignored so + /// a late teardown cannot wipe a successor's host. + /// + void Unregister(long modalId); +} diff --git a/src/EventLogExpert.UI/Alerts/InlineAlertHostBroker.cs b/src/EventLogExpert.UI/Alerts/InlineAlertHostBroker.cs new file mode 100644 index 00000000..e744f105 --- /dev/null +++ b/src/EventLogExpert.UI/Alerts/InlineAlertHostBroker.cs @@ -0,0 +1,57 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Modal; + +namespace EventLogExpert.UI.Alerts; + +public sealed class InlineAlertHostBroker(IModalService modalService) : IInlineAlertHostBroker +{ + private readonly IModalService _modalService = modalService; + private readonly Lock _stateLock = new(); + + private long _activeId; + private IInlineAlertHost? _host; + + public void Register(long modalId, IInlineAlertHost host) + { + ArgumentNullException.ThrowIfNull(host); + + lock (_stateLock) + { + // Late registration from a stale modal would route alerts to a torn-down host. + if (modalId != _modalService.ActiveModalId) { return; } + + _host = host; + _activeId = modalId; + } + } + + public bool TryGet(out IInlineAlertHost? host) + { + lock (_stateLock) + { + // Lazy invalidation: if the active modal has changed since the last register/unregister, the recorded + // host belongs to a torn-down modal. Drop it instead of routing the alert there. + if (_activeId != _modalService.ActiveModalId) + { + _host = null; + _activeId = 0; + } + + host = _host; + return host is not null; + } + } + + public void Unregister(long modalId) + { + lock (_stateLock) + { + if (modalId != _activeId) { return; } + + _host = null; + _activeId = 0; + } + } +} diff --git a/src/EventLogExpert.UI/Alerts/InlineAlertRequest.cs b/src/EventLogExpert.UI/Alerts/InlineAlertRequest.cs new file mode 100644 index 00000000..4c1b9e78 --- /dev/null +++ b/src/EventLogExpert.UI/Alerts/InlineAlertRequest.cs @@ -0,0 +1,16 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Alerts; + +/// +/// Describes an alert/prompt hosted inline by an active modal. AcceptLabel null means dismiss-only; +/// IsPrompt true renders an input field below the message. +/// +public sealed record InlineAlertRequest( + string Title, + string Message, + string? AcceptLabel, + string CancelLabel, + bool IsPrompt, + string? PromptInitialValue); diff --git a/src/EventLogExpert.UI/Alerts/InlineAlertResult.cs b/src/EventLogExpert.UI/Alerts/InlineAlertResult.cs new file mode 100644 index 00000000..44ccb47a --- /dev/null +++ b/src/EventLogExpert.UI/Alerts/InlineAlertResult.cs @@ -0,0 +1,7 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Alerts; + +/// Result of an inline alert. is non-null only for prompt requests. +public sealed record InlineAlertResult(bool Accepted, string? PromptValue); diff --git a/src/EventLogExpert.UI/Banner/BannerId.cs b/src/EventLogExpert.UI/Banner/BannerId.cs new file mode 100644 index 00000000..93dafd70 --- /dev/null +++ b/src/EventLogExpert.UI/Banner/BannerId.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Banner; + +public readonly record struct BannerId(Guid Value) +{ + public static BannerId Create() => new(Guid.NewGuid()); +} diff --git a/src/EventLogExpert.UI/Models/BannerInfoEntry.cs b/src/EventLogExpert.UI/Banner/BannerInfoEntry.cs similarity index 80% rename from src/EventLogExpert.UI/Models/BannerInfoEntry.cs rename to src/EventLogExpert.UI/Banner/BannerInfoEntry.cs index 983df6cb..7bb978ec 100644 --- a/src/EventLogExpert.UI/Models/BannerInfoEntry.cs +++ b/src/EventLogExpert.UI/Banner/BannerInfoEntry.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Banner; public sealed record BannerInfoEntry( - Guid Id, + BannerId Id, string Title, string Message, BannerSeverity Severity, diff --git a/src/EventLogExpert.UI/Models/BannerProgressEntry.cs b/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs similarity index 75% rename from src/EventLogExpert.UI/Models/BannerProgressEntry.cs rename to src/EventLogExpert.UI/Banner/BannerProgressEntry.cs index f45b1b71..43d7df9a 100644 --- a/src/EventLogExpert.UI/Models/BannerProgressEntry.cs +++ b/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs @@ -1,10 +1,12 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +using EventLogExpert.UI.Database.Upgrade; + +namespace EventLogExpert.UI.Banner; public sealed record BannerProgressEntry( - Guid BatchId, + UpgradeBatchId BatchId, UpgradeProgressScope Scope, int CurrentBatchPosition, int CurrentBatchSize, diff --git a/src/EventLogExpert.UI/Services/BannerService.cs b/src/EventLogExpert.UI/Banner/BannerService.cs similarity index 95% rename from src/EventLogExpert.UI/Services/BannerService.cs rename to src/EventLogExpert.UI/Banner/BannerService.cs index 829f41d8..f43a483b 100644 --- a/src/EventLogExpert.UI/Services/BannerService.cs +++ b/src/EventLogExpert.UI/Banner/BannerService.cs @@ -2,11 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; using System.Collections.Immutable; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Banner; public sealed class BannerService : IBannerService { @@ -109,7 +109,7 @@ public void DismissAttention() } } - public void DismissError(Guid id) + public void DismissError(BannerId id) { bool removed; @@ -126,7 +126,7 @@ public void DismissError(Guid id) } } - public void DismissInfoBanner(Guid id) + public void DismissInfoBanner(BannerId id) { bool removed; @@ -170,7 +170,7 @@ public void ReportCritical(Exception ex) RaiseStateChanged(); } - public Guid ReportError(string title, string message, string? actionLabel = null, Func? action = null) + public BannerId ReportError(string title, string message, string? actionLabel = null, Func? action = null) { bool hasAction = action is not null; bool hasLabel = !string.IsNullOrWhiteSpace(actionLabel); @@ -184,7 +184,7 @@ public Guid ReportError(string title, string message, string? actionLabel = null string? normalizedLabel = hasLabel ? actionLabel : null; Func? normalizedAction = hasAction ? action : null; - var entry = new ErrorBannerEntry(Guid.NewGuid(), title, message, normalizedLabel, normalizedAction, DateTime.UtcNow); + var entry = new ErrorBannerEntry(BannerId.Create(), title, message, normalizedLabel, normalizedAction, DateTime.UtcNow); lock (_stateLock) { @@ -197,7 +197,7 @@ public Guid ReportError(string title, string message, string? actionLabel = null public void ReportInfoBanner(string title, string message, BannerSeverity severity) { - var entry = new BannerInfoEntry(Guid.NewGuid(), title, message, severity, DateTime.UtcNow); + var entry = new BannerInfoEntry(BannerId.Create(), title, message, severity, DateTime.UtcNow); lock (_stateLock) { diff --git a/src/EventLogExpert.UI/Models/BannerSeverity.cs b/src/EventLogExpert.UI/Banner/BannerSeverity.cs similarity index 79% rename from src/EventLogExpert.UI/Models/BannerSeverity.cs rename to src/EventLogExpert.UI/Banner/BannerSeverity.cs index eddf50bc..4d4ad641 100644 --- a/src/EventLogExpert.UI/Models/BannerSeverity.cs +++ b/src/EventLogExpert.UI/Banner/BannerSeverity.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Banner; public enum BannerSeverity { diff --git a/src/EventLogExpert.UI/Services/BannerViewSelector.cs b/src/EventLogExpert.UI/Banner/BannerViewSelector.cs similarity index 94% rename from src/EventLogExpert.UI/Services/BannerViewSelector.cs rename to src/EventLogExpert.UI/Banner/BannerViewSelector.cs index dd67d16f..c0f373ad 100644 --- a/src/EventLogExpert.UI/Services/BannerViewSelector.cs +++ b/src/EventLogExpert.UI/Banner/BannerViewSelector.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Banner; public enum BannerView { @@ -15,7 +15,7 @@ public enum BannerView Info } -public sealed record BannerCycleItem(BannerView View, int IndexWithinSlice, Guid? EntryId); +public sealed record BannerCycleItem(BannerView View, int IndexWithinSlice, BannerId? EntryId); public static class BannerViewSelector { diff --git a/src/EventLogExpert.UI/Models/ErrorBannerEntry.cs b/src/EventLogExpert.UI/Banner/ErrorBannerEntry.cs similarity index 81% rename from src/EventLogExpert.UI/Models/ErrorBannerEntry.cs rename to src/EventLogExpert.UI/Banner/ErrorBannerEntry.cs index a6327afd..2bf54306 100644 --- a/src/EventLogExpert.UI/Models/ErrorBannerEntry.cs +++ b/src/EventLogExpert.UI/Banner/ErrorBannerEntry.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Banner; public sealed record ErrorBannerEntry( - Guid Id, + BannerId Id, string Title, string Message, string? ActionLabel, diff --git a/src/EventLogExpert.UI/Interfaces/IBannerService.cs b/src/EventLogExpert.UI/Banner/IBannerService.cs similarity index 77% rename from src/EventLogExpert.UI/Interfaces/IBannerService.cs rename to src/EventLogExpert.UI/Banner/IBannerService.cs index 0b53c7fc..a31bcfa6 100644 --- a/src/EventLogExpert.UI/Interfaces/IBannerService.cs +++ b/src/EventLogExpert.UI/Banner/IBannerService.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Banner; public interface IBannerService { @@ -27,16 +27,16 @@ public interface IBannerService void DismissAttention(); - void DismissError(Guid id); + void DismissError(BannerId id); /// Remove an info banner by id and raise . No-op if the id is not present. - void DismissInfoBanner(Guid id); + void DismissInfoBanner(BannerId id); IDisposable RegisterRecoveryCallback(Func recover); void ReportCritical(Exception ex); - Guid ReportError(string title, string message, string? actionLabel = null, Func? action = null); + BannerId ReportError(string title, string message, string? actionLabel = null, Func? action = null); void ReportInfoBanner(string title, string message, BannerSeverity severity); diff --git a/src/EventLogExpert.UI/Services/AppTitleService.cs b/src/EventLogExpert.UI/Common/AppTitle/AppTitleService.cs similarity index 85% rename from src/EventLogExpert.UI/Services/AppTitleService.cs rename to src/EventLogExpert.UI/Common/AppTitle/AppTitleService.cs index d7f99e6c..d08be335 100644 --- a/src/EventLogExpert.UI/Services/AppTitleService.cs +++ b/src/EventLogExpert.UI/Common/AppTitle/AppTitleService.cs @@ -1,19 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Versioning; using System.Text; -namespace EventLogExpert.UI.Services; - -public interface IAppTitleService -{ - void SetIsPrerelease(bool isPrerelease); - - void SetLogName(string? logName); - - void SetProgressString(string? progressString); -} +namespace EventLogExpert.UI.Common.AppTitle; public sealed class AppTitleService( ICurrentVersionProvider versionProvider, diff --git a/src/EventLogExpert.UI/Common/AppTitle/IAppTitleService.cs b/src/EventLogExpert.UI/Common/AppTitle/IAppTitleService.cs new file mode 100644 index 00000000..c01fd6ee --- /dev/null +++ b/src/EventLogExpert.UI/Common/AppTitle/IAppTitleService.cs @@ -0,0 +1,13 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Common.AppTitle; + +public interface IAppTitleService +{ + void SetIsPrerelease(bool isPrerelease); + + void SetLogName(string? logName); + + void SetProgressString(string? progressString); +} diff --git a/src/EventLogExpert.UI/Interfaces/ITitleProvider.cs b/src/EventLogExpert.UI/Common/AppTitle/ITitleProvider.cs similarity index 79% rename from src/EventLogExpert.UI/Interfaces/ITitleProvider.cs rename to src/EventLogExpert.UI/Common/AppTitle/ITitleProvider.cs index 6956da10..ce773737 100644 --- a/src/EventLogExpert.UI/Interfaces/ITitleProvider.cs +++ b/src/EventLogExpert.UI/Common/AppTitle/ITitleProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.AppTitle; public interface ITitleProvider { diff --git a/src/EventLogExpert.UI/Common/Clipboard/EventCopyFormat.cs b/src/EventLogExpert.UI/Common/Clipboard/EventCopyFormat.cs new file mode 100644 index 00000000..3c35abd5 --- /dev/null +++ b/src/EventLogExpert.UI/Common/Clipboard/EventCopyFormat.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Common.Clipboard; + +public enum EventCopyFormat +{ + Default, + Simple, + Xml, + Full +} diff --git a/src/EventLogExpert.UI/Common/Clipboard/IClipboardService.cs b/src/EventLogExpert.UI/Common/Clipboard/IClipboardService.cs new file mode 100644 index 00000000..b5bc9bd6 --- /dev/null +++ b/src/EventLogExpert.UI/Common/Clipboard/IClipboardService.cs @@ -0,0 +1,20 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Common.Clipboard; + +public interface IClipboardService +{ + /// + /// Best-effort copy of the current selection (one or more events), or the focused event when nothing is selected, + /// to the clipboard. Implementations must not throw; any failure is logged internally so callers can invoke without + /// try/catch. + /// + Task CopySelectedEvent(EventCopyFormat? format = null); + + /// + /// Best-effort copy of the supplied text to the clipboard. Implementations must not throw; any failure is logged + /// internally so callers can invoke without try/catch. + /// + Task CopyTextAsync(string text); +} diff --git a/src/EventLogExpert.UI/Models/DisplayConverter.cs b/src/EventLogExpert.UI/Common/Display/DisplayConverter.cs similarity index 82% rename from src/EventLogExpert.UI/Models/DisplayConverter.cs rename to src/EventLogExpert.UI/Common/Display/DisplayConverter.cs index e0b5b0b2..3934808f 100644 --- a/src/EventLogExpert.UI/Models/DisplayConverter.cs +++ b/src/EventLogExpert.UI/Common/Display/DisplayConverter.cs @@ -1,9 +1,9 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Common.Display; -public class DisplayConverter +public sealed class DisplayConverter { public Func? GetFunc { get; set; } diff --git a/src/EventLogExpert.UI/ExtensionMethods.cs b/src/EventLogExpert.UI/Common/Display/DisplayExtensions.cs similarity index 91% rename from src/EventLogExpert.UI/ExtensionMethods.cs rename to src/EventLogExpert.UI/Common/Display/DisplayExtensions.cs index 65b52899..59607fc0 100644 --- a/src/EventLogExpert.UI/ExtensionMethods.cs +++ b/src/EventLogExpert.UI/Common/Display/DisplayExtensions.cs @@ -4,9 +4,9 @@ using System.Reflection; using System.Runtime.Serialization; -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Common.Display; -public static class ExtensionMethods +public static class DisplayExtensions { public static string ToFullString(this Enum value) { diff --git a/src/EventLogExpert.UI/Services/ReversedListView.cs b/src/EventLogExpert.UI/Common/Display/ReversedListView.cs similarity index 97% rename from src/EventLogExpert.UI/Services/ReversedListView.cs rename to src/EventLogExpert.UI/Common/Display/ReversedListView.cs index 5372574a..702be536 100644 --- a/src/EventLogExpert.UI/Services/ReversedListView.cs +++ b/src/EventLogExpert.UI/Common/Display/ReversedListView.cs @@ -3,7 +3,7 @@ using System.Collections; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Display; public sealed class ReversedListView : IReadOnlyList, IList { diff --git a/src/EventLogExpert.UI/Options/FileLocationOptions.cs b/src/EventLogExpert.UI/Common/Files/FileLocationOptions.cs similarity index 68% rename from src/EventLogExpert.UI/Options/FileLocationOptions.cs rename to src/EventLogExpert.UI/Common/Files/FileLocationOptions.cs index 7bc201e2..4838ae0e 100644 --- a/src/EventLogExpert.UI/Options/FileLocationOptions.cs +++ b/src/EventLogExpert.UI/Common/Files/FileLocationOptions.cs @@ -1,9 +1,9 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Options; +namespace EventLogExpert.UI.Common.Files; -public class FileLocationOptions(string basePath) +public sealed class FileLocationOptions(string basePath) { private readonly string _basePath = basePath; diff --git a/src/EventLogExpert.UI/Common/Files/FilePickerFileTypes.cs b/src/EventLogExpert.UI/Common/Files/FilePickerFileTypes.cs new file mode 100644 index 00000000..5c0c9abc --- /dev/null +++ b/src/EventLogExpert.UI/Common/Files/FilePickerFileTypes.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Common.Files; + +public static class FilePickerFileTypes +{ + public static readonly IReadOnlyList Database = [".db", ".zip"]; + public static readonly IReadOnlyList Json = [".json"]; +} diff --git a/src/EventLogExpert.UI/Common/Files/FileSaveFileTypes.cs b/src/EventLogExpert.UI/Common/Files/FileSaveFileTypes.cs new file mode 100644 index 00000000..e9d68376 --- /dev/null +++ b/src/EventLogExpert.UI/Common/Files/FileSaveFileTypes.cs @@ -0,0 +1,17 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Common.Files; + +public static class FileSaveFileTypes +{ + public static readonly IReadOnlyDictionary> Json = + ImmutableDictionary>.Empty + .Add("JSON", [".json"]); + + public static readonly IReadOnlyDictionary> Log = + ImmutableDictionary>.Empty + .Add("Log files", [".log", ".txt"]); +} diff --git a/src/EventLogExpert.UI/Interfaces/IFilePickerService.cs b/src/EventLogExpert.UI/Common/Files/IFilePickerService.cs similarity index 64% rename from src/EventLogExpert.UI/Interfaces/IFilePickerService.cs rename to src/EventLogExpert.UI/Common/Files/IFilePickerService.cs index 24335b20..fd16ba93 100644 --- a/src/EventLogExpert.UI/Interfaces/IFilePickerService.cs +++ b/src/EventLogExpert.UI/Common/Files/IFilePickerService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Files; public interface IFilePickerService { @@ -12,14 +12,8 @@ public interface IFilePickerService Task PickAsync(string pickerTitle, IReadOnlyList extensions); /// - /// Opens a multi-select "Open File" dialog filtered to ; returns the picked - /// paths (empty if the user cancelled). + /// Opens a multi-select "Open File" dialog filtered to ; returns the picked paths + /// (empty if the user cancelled). /// Task> PickMultipleAsync(string pickerTitle, IReadOnlyList extensions); } - -public static class FilePickerServiceFileTypes -{ - public static readonly IReadOnlyList Database = [".db", ".zip"]; - public static readonly IReadOnlyList Json = [".json"]; -} diff --git a/src/EventLogExpert.UI/Common/Files/IFileSaveService.cs b/src/EventLogExpert.UI/Common/Files/IFileSaveService.cs new file mode 100644 index 00000000..f6475931 --- /dev/null +++ b/src/EventLogExpert.UI/Common/Files/IFileSaveService.cs @@ -0,0 +1,34 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Alerts; + +namespace EventLogExpert.UI.Common.Files; + +public interface IFileSaveService +{ + /// + /// Opens a system "Save As" dialog and, only after the user confirms a destination, invokes + /// against a writable stream whose contents are then saved to the destination. + /// Returns the saved path on success, or null if the user cancelled (in which case + /// is never invoked). Throws if the user picked a path but the write or completion + /// failed, or if the host environment cannot present a save dialog (e.g., no MAUI window available); callers should + /// handle failures via try/catch and surface them through . + /// + /// Default filename shown in the dialog (e.g., "debug-log.log"). + /// + /// Group label to extension list (e.g., "Log files" -> [".log", ".txt"]). Must contain at least + /// one entry. + /// + /// + /// Asynchronous writer invoked exactly once with a writable stream after the user confirms a + /// destination. Implementations may stream directly to disk or buffer the entire output in memory before writing to + /// the picked file (the MAUI implementation buffers so that a writer exception leaves the picked file untouched); + /// callers should keep exports reasonably bounded to fit in memory. The stream is owned by the service and disposed + /// once the writer completes; callers must not retain or dispose it. + /// + Task SaveAsync( + string suggestedFileName, + IReadOnlyDictionary> fileTypes, + Func writeContent); +} diff --git a/src/EventLogExpert.UI/Interfaces/IWindowsIdentityProvider.cs b/src/EventLogExpert.UI/Common/Identity/IWindowsIdentityProvider.cs similarity index 78% rename from src/EventLogExpert.UI/Interfaces/IWindowsIdentityProvider.cs rename to src/EventLogExpert.UI/Common/Identity/IWindowsIdentityProvider.cs index c176dd2e..ed74df93 100644 --- a/src/EventLogExpert.UI/Interfaces/IWindowsIdentityProvider.cs +++ b/src/EventLogExpert.UI/Common/Identity/IWindowsIdentityProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Identity; public interface IWindowsIdentityProvider { diff --git a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs b/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs similarity index 58% rename from src/EventLogExpert.UI/Store/LoggingMiddleware.cs rename to src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs index f2905586..4dabe402 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs @@ -2,13 +2,13 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Store.EventLog; -using EventLogExpert.UI.Store.EventTable; -using EventLogExpert.UI.Store.StatusBar; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.LogTable; +using EventLogExpert.UI.StatusBar; using Fluxor; using System.Text.Json; -namespace EventLogExpert.UI.Store; +namespace EventLogExpert.UI.Common.Logging; public sealed class LoggingMiddleware(ITraceLogger debugLogger) : Middleware { @@ -18,35 +18,35 @@ public override void BeforeDispatch(object action) { switch (action) { - case EventLogAction.LoadEvents loadEventsAction: + case LoadEventsAction loadEventsAction: _debugLogger.Debug($"Action: {action.GetType()} with {loadEventsAction.Events.Count} events."); break; - case EventLogAction.LoadEventsPartial loadEventsPartialAction: + case LoadEventsPartialAction loadEventsPartialAction: _debugLogger.Debug($"Action: {action.GetType()} with {loadEventsPartialAction.Events.Count} events."); break; - case EventLogAction.AddEvent addEventAction: + case AddEventAction addEventAction: _debugLogger.Debug($"Action: {action.GetType()} with {addEventAction.NewEvent.Source} event ID {addEventAction.NewEvent.Id}."); break; - case EventLogAction.OpenLog openLogAction: + case OpenLogAction openLogAction: _debugLogger.Information($"Action: {action.GetType()} with {openLogAction.LogName} log type {openLogAction.LogPathType}."); break; - case EventLogAction.SelectEvent selectEventAction: - _debugLogger.Debug($"Action: {nameof(EventLogAction.SelectEvent)} selected {selectEventAction.SelectedEvent.Source} event ID {selectEventAction.SelectedEvent.Id}."); + case SelectEventAction selectEventAction: + _debugLogger.Debug($"Action: {nameof(SelectEventAction)} selected {selectEventAction.SelectedEvent.Source} event ID {selectEventAction.SelectedEvent.Id}."); break; - case EventLogAction.SelectEvents selectEventsAction: - _debugLogger.Debug($"Action: {nameof(EventLogAction.SelectEvents)} selected {selectEventsAction.SelectedEvents.Count} events."); + case SelectEventsAction selectEventsAction: + _debugLogger.Debug($"Action: {nameof(SelectEventsAction)} selected {selectEventsAction.SelectedEvents.Count} events."); break; - case EventLogAction.SetSelectedEvents setSelectedEventsAction: - _debugLogger.Debug($"Action: {nameof(EventLogAction.SetSelectedEvents)} set {setSelectedEventsAction.SelectedEvents.Count} events."); + case SetSelectedEventsAction setSelectedEventsAction: + _debugLogger.Debug($"Action: {nameof(SetSelectedEventsAction)} set {setSelectedEventsAction.SelectedEvents.Count} events."); break; - case EventTableAction.AppendTableEvents appendTableEventsAction: + case AppendTableEventsAction appendTableEventsAction: _debugLogger.Debug($"Action: {action.GetType()} with {appendTableEventsAction.Events.Count} events for log {appendTableEventsAction.LogId}."); break; - case EventTableAction.AppendTableEventsBatch appendTableEventsBatchAction: + case AppendTableEventsBatchAction appendTableEventsBatchAction: var totalAppendEvents = appendTableEventsBatchAction.EventsByLog.Values.Sum(eventsForLog => eventsForLog.Count); _debugLogger.Debug($"Action: {action.GetType()} with {totalAppendEvents} events across {appendTableEventsBatchAction.EventsByLog.Count} logs."); break; - case StatusBarAction.SetEventsLoading: + case SetEventsLoadingAction: _debugLogger.Debug($"Action: {action.GetType()} {JsonSerializer.Serialize(action)}"); break; default: diff --git a/src/EventLogExpert.UI/Services/ReleaseNotesMarkdownRenderer.cs b/src/EventLogExpert.UI/Common/Markdown/MarkdownRenderer.cs similarity index 94% rename from src/EventLogExpert.UI/Services/ReleaseNotesMarkdownRenderer.cs rename to src/EventLogExpert.UI/Common/Markdown/MarkdownRenderer.cs index d05b0090..26b1c41a 100644 --- a/src/EventLogExpert.UI/Services/ReleaseNotesMarkdownRenderer.cs +++ b/src/EventLogExpert.UI/Common/Markdown/MarkdownRenderer.cs @@ -6,14 +6,13 @@ using System.Text; using System.Text.RegularExpressions; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Markdown; /// -/// Renders a small, well-defined subset of Markdown to safe HTML for the in-app -/// release notes modal. Inputs are HTML-escaped first; only the renderer itself -/// emits structural tags. Raw HTML in the input is treated as text. +/// Renders a small, well-defined subset of Markdown to safe HTML for the in-app release notes modal. Inputs are +/// HTML-escaped first; only the renderer itself emits structural tags. Raw HTML in the input is treated as text. /// -public static partial class ReleaseNotesMarkdownRenderer +public static partial class MarkdownRenderer { private const char CodePlaceholderSentinel = '\u0001'; diff --git a/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs b/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs similarity index 69% rename from src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs rename to src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs index 7d8563c5..f227266b 100644 --- a/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs +++ b/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs @@ -1,10 +1,13 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.LogTable; +using EventLogExpert.UI.Settings; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Preferences; public interface IPreferencesProvider { @@ -22,7 +25,7 @@ public interface IPreferencesProvider IEnumerable FavoriteFiltersPreference { get; set; } - CopyType KeyboardCopyTypePreference { get; set; } + EventCopyFormat KeyboardCopyFormatPreference { get; set; } LogLevel LogLevelPreference { get; set; } @@ -30,7 +33,7 @@ public interface IPreferencesProvider IEnumerable RecentFiltersPreference { get; set; } - IEnumerable SavedFiltersPreference { get; set; } + IEnumerable SavedFiltersPreference { get; set; } Theme ThemePreference { get; set; } diff --git a/src/EventLogExpert.UI/Interfaces/IApplicationRestartService.cs b/src/EventLogExpert.UI/Common/Restart/IApplicationRestartService.cs similarity index 89% rename from src/EventLogExpert.UI/Interfaces/IApplicationRestartService.cs rename to src/EventLogExpert.UI/Common/Restart/IApplicationRestartService.cs index 98da40e6..6320318b 100644 --- a/src/EventLogExpert.UI/Interfaces/IApplicationRestartService.cs +++ b/src/EventLogExpert.UI/Common/Restart/IApplicationRestartService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Restart; public interface IApplicationRestartService { diff --git a/src/EventLogExpert.UI/Common/Threading/IMainThreadService.cs b/src/EventLogExpert.UI/Common/Threading/IMainThreadService.cs new file mode 100644 index 00000000..500715a2 --- /dev/null +++ b/src/EventLogExpert.UI/Common/Threading/IMainThreadService.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Common.Threading; + +public interface IMainThreadService +{ + Task InvokeOnMainThread(Action action); + + /// Invoke an asynchronous delegate on the main thread and await it. + Task InvokeOnMainThreadAsync(Func action); +} diff --git a/src/EventLogExpert.UI/Services/CurrentVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs similarity index 74% rename from src/EventLogExpert.UI/Services/CurrentVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs index 4c381690..789ea339 100644 --- a/src/EventLogExpert.UI/Services/CurrentVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs @@ -1,20 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Identity; -namespace EventLogExpert.UI.Services; - -public interface ICurrentVersionProvider -{ - Version CurrentVersion { get; } - - bool IsAdmin { get; } - - bool IsDevBuild { get; } - - bool IsSupportedOS(Version currentVersion); -} +namespace EventLogExpert.UI.Common.Versioning; public sealed class CurrentVersionProvider( IPackageVersionProvider packageVersionProvider, diff --git a/src/EventLogExpert.UI/Common/Versioning/ICurrentVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/ICurrentVersionProvider.cs new file mode 100644 index 00000000..c37eee79 --- /dev/null +++ b/src/EventLogExpert.UI/Common/Versioning/ICurrentVersionProvider.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Common.Versioning; + +public interface ICurrentVersionProvider +{ + Version CurrentVersion { get; } + + bool IsAdmin { get; } + + bool IsDevBuild { get; } + + bool IsSupportedOS(Version currentVersion); +} diff --git a/src/EventLogExpert.UI/Interfaces/IPackageVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/IPackageVersionProvider.cs similarity index 77% rename from src/EventLogExpert.UI/Interfaces/IPackageVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/IPackageVersionProvider.cs index 00a14fe3..d6f1ed12 100644 --- a/src/EventLogExpert.UI/Interfaces/IPackageVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/IPackageVersionProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Versioning; public interface IPackageVersionProvider { diff --git a/src/EventLogExpert.UI/Services/PackageVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/PackageVersionProvider.cs similarity index 61% rename from src/EventLogExpert.UI/Services/PackageVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/PackageVersionProvider.cs index f7fb1042..0c09542a 100644 --- a/src/EventLogExpert.UI/Services/PackageVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/PackageVersionProvider.cs @@ -1,16 +1,15 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; using Windows.ApplicationModel; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Versioning; public sealed class PackageVersionProvider : IPackageVersionProvider { public Version GetPackageVersion() { PackageVersion packageVersion = Package.Current.Id.Version; - return new($"{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}.{packageVersion.Revision}"); + return new Version($"{packageVersion.Major}.{packageVersion.Minor}.{packageVersion.Build}.{packageVersion.Revision}"); } } diff --git a/src/EventLogExpert.UI/Models/DatabaseEntry.cs b/src/EventLogExpert.UI/Database/DatabaseEntry.cs similarity index 86% rename from src/EventLogExpert.UI/Models/DatabaseEntry.cs rename to src/EventLogExpert.UI/Database/DatabaseEntry.cs index 5f923961..68d10ba1 100644 --- a/src/EventLogExpert.UI/Models/DatabaseEntry.cs +++ b/src/EventLogExpert.UI/Database/DatabaseEntry.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database; public sealed record DatabaseEntry( string FileName, diff --git a/src/EventLogExpert.UI/Services/DatabaseService.cs b/src/EventLogExpert.UI/Database/DatabaseService.cs similarity index 99% rename from src/EventLogExpert.UI/Services/DatabaseService.cs rename to src/EventLogExpert.UI/Database/DatabaseService.cs index f017cf4b..202a9157 100644 --- a/src/EventLogExpert.UI/Services/DatabaseService.cs +++ b/src/EventLogExpert.UI/Database/DatabaseService.cs @@ -1,18 +1,18 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Databases; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.ProviderDatabase; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Options; +using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Common.Preferences; +using EventLogExpert.UI.Database.Upgrade; using Microsoft.Data.Sqlite; using System.Collections.Immutable; using System.IO.Compression; using System.Threading.Channels; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Database; public sealed class DatabaseService : IDatabaseService, IActiveDatabasePathsProvider { @@ -728,7 +728,7 @@ public async Task UpgradeBatchAsync( var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var batch = new UpgradeBatch( - Guid.NewGuid(), + UpgradeBatchId.Create(), scope, acceptable, tcs, @@ -1300,7 +1300,7 @@ private bool UpdateEntryStatusAndBackup(string fileName, DatabaseStatus status, return true; } - private async Task UpgradeAsync(string fileName, int position, Guid batchId, CancellationToken cancellationToken) + private async Task UpgradeAsync(string fileName, int position, UpgradeBatchId batchId, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -1512,7 +1512,7 @@ private sealed class UpgradeCleanupFailedException(string message) : Exception(m private sealed class UpgradeRollbackFailedException(string message) : Exception(message); private sealed record UpgradeBatch( - Guid Id, + UpgradeBatchId Id, UpgradeProgressScope Scope, IReadOnlyList Entries, TaskCompletionSource Tcs, diff --git a/src/EventLogExpert.UI/Database/DatabaseStatus.cs b/src/EventLogExpert.UI/Database/DatabaseStatus.cs new file mode 100644 index 00000000..bb87580a --- /dev/null +++ b/src/EventLogExpert.UI/Database/DatabaseStatus.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Database; + +public enum DatabaseStatus +{ + NotClassified, + Ready, + UpgradeRequired, + UpgradeFailed, + UnrecognizedSchema, + ObsoleteSchema, + ClassificationFailed +} diff --git a/src/EventLogExpert.UI/DatabaseStatusLabels.cs b/src/EventLogExpert.UI/Database/DatabaseStatusLabels.cs similarity index 92% rename from src/EventLogExpert.UI/DatabaseStatusLabels.cs rename to src/EventLogExpert.UI/Database/DatabaseStatusLabels.cs index 12a9f59b..9c21ac70 100644 --- a/src/EventLogExpert.UI/DatabaseStatusLabels.cs +++ b/src/EventLogExpert.UI/Database/DatabaseStatusLabels.cs @@ -1,9 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; - -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Database; public static class DatabaseStatusLabels { diff --git a/src/EventLogExpert.UI/Interfaces/IDatabaseService.cs b/src/EventLogExpert.UI/Database/IDatabaseService.cs similarity index 93% rename from src/EventLogExpert.UI/Interfaces/IDatabaseService.cs rename to src/EventLogExpert.UI/Database/IDatabaseService.cs index e745ea0d..bf9b143b 100644 --- a/src/EventLogExpert.UI/Interfaces/IDatabaseService.cs +++ b/src/EventLogExpert.UI/Database/IDatabaseService.cs @@ -1,9 +1,9 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database.Upgrade; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Database; public interface IDatabaseService : IAsyncDisposable { diff --git a/src/EventLogExpert.UI/Models/ImportFailure.cs b/src/EventLogExpert.UI/Database/ImportFailure.cs similarity index 79% rename from src/EventLogExpert.UI/Models/ImportFailure.cs rename to src/EventLogExpert.UI/Database/ImportFailure.cs index 3521461a..c04facfb 100644 --- a/src/EventLogExpert.UI/Models/ImportFailure.cs +++ b/src/EventLogExpert.UI/Database/ImportFailure.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database; public sealed record ImportFailure(string FileName, string Reason); diff --git a/src/EventLogExpert.UI/Models/ImportResult.cs b/src/EventLogExpert.UI/Database/ImportResult.cs similarity index 85% rename from src/EventLogExpert.UI/Models/ImportResult.cs rename to src/EventLogExpert.UI/Database/ImportResult.cs index bdcf8ec8..18a5acdb 100644 --- a/src/EventLogExpert.UI/Models/ImportResult.cs +++ b/src/EventLogExpert.UI/Database/ImportResult.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database; public sealed record ImportResult( int Imported, diff --git a/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchCompletedEventArgs.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchCompletedEventArgs.cs new file mode 100644 index 00000000..c370c4c9 --- /dev/null +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchCompletedEventArgs.cs @@ -0,0 +1,13 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Database.Upgrade; + +public sealed class UpgradeBatchCompletedEventArgs(UpgradeBatchId batchId, UpgradeBatchResult result, bool wasCancelled) : EventArgs +{ + public UpgradeBatchId BatchId { get; } = batchId; + + public UpgradeBatchResult Result { get; } = result; + + public bool WasCancelled { get; } = wasCancelled; +} diff --git a/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchId.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchId.cs new file mode 100644 index 00000000..0ce9655a --- /dev/null +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchId.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Database.Upgrade; + +public readonly record struct UpgradeBatchId(Guid Value) +{ + public static UpgradeBatchId Create() => new(Guid.NewGuid()); +} diff --git a/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchProgressEventArgs.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchProgressEventArgs.cs new file mode 100644 index 00000000..613b828d --- /dev/null +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchProgressEventArgs.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Database.Upgrade; + +public sealed class UpgradeBatchProgressEventArgs(UpgradeBatchId batchId, int position, string fileName, UpgradePhase phase) : EventArgs +{ + public UpgradeBatchId BatchId { get; } = batchId; + + public string FileName { get; } = fileName; + + public UpgradePhase Phase { get; } = phase; + + public int Position { get; } = position; +} diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchResult.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchResult.cs similarity index 83% rename from src/EventLogExpert.UI/Models/UpgradeBatchResult.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchResult.cs index 9dd5c51f..2538c25e 100644 --- a/src/EventLogExpert.UI/Models/UpgradeBatchResult.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchResult.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database.Upgrade; public sealed record UpgradeBatchResult( IReadOnlyList Succeeded, diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchStartedEventArgs.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchStartedEventArgs.cs similarity index 82% rename from src/EventLogExpert.UI/Models/UpgradeBatchStartedEventArgs.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchStartedEventArgs.cs index 721a86c6..c85c43b4 100644 --- a/src/EventLogExpert.UI/Models/UpgradeBatchStartedEventArgs.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchStartedEventArgs.cs @@ -1,17 +1,17 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database.Upgrade; public sealed class UpgradeBatchStartedEventArgs( - Guid batchId, + UpgradeBatchId batchId, UpgradeProgressScope scope, int batchSize, CancellationTokenSource cts) : EventArgs { private readonly CancellationTokenSource _cts = cts; - public Guid BatchId { get; } = batchId; + public UpgradeBatchId BatchId { get; } = batchId; public int BatchSize { get; } = batchSize; diff --git a/src/EventLogExpert.UI/Models/UpgradeFailure.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeFailure.cs similarity index 76% rename from src/EventLogExpert.UI/Models/UpgradeFailure.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeFailure.cs index 76b94aa0..de3ab1dd 100644 --- a/src/EventLogExpert.UI/Models/UpgradeFailure.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeFailure.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database.Upgrade; public sealed record UpgradeFailure(string FileName, string Message); diff --git a/src/EventLogExpert.UI/Models/UpgradePhase.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradePhase.cs similarity index 77% rename from src/EventLogExpert.UI/Models/UpgradePhase.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradePhase.cs index 36ffb9cd..1d901d3a 100644 --- a/src/EventLogExpert.UI/Models/UpgradePhase.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradePhase.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database.Upgrade; public enum UpgradePhase { diff --git a/src/EventLogExpert.UI/Models/UpgradeProgressScope.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeProgressScope.cs similarity index 77% rename from src/EventLogExpert.UI/Models/UpgradeProgressScope.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeProgressScope.cs index d7dbade3..f026300e 100644 --- a/src/EventLogExpert.UI/Models/UpgradeProgressScope.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeProgressScope.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Database.Upgrade; public enum UpgradeProgressScope { diff --git a/src/EventLogExpert.UI/Models/DebugLogEntry.cs b/src/EventLogExpert.UI/DebugLog/DebugLogEntry.cs similarity index 89% rename from src/EventLogExpert.UI/Models/DebugLogEntry.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogEntry.cs index fbeef451..2af12e90 100644 --- a/src/EventLogExpert.UI/Models/DebugLogEntry.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogEntry.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.DebugLog; public sealed record DebugLogEntry( DateTimeOffset? Timestamp, diff --git a/src/EventLogExpert.UI/Services/DebugLogEntryParser.cs b/src/EventLogExpert.UI/DebugLog/DebugLogEntryParser.cs similarity index 98% rename from src/EventLogExpert.UI/Services/DebugLogEntryParser.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogEntryParser.cs index 09911fd2..53230b6d 100644 --- a/src/EventLogExpert.UI/Services/DebugLogEntryParser.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogEntryParser.cs @@ -1,13 +1,12 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; using Microsoft.Extensions.Logging; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Text.RegularExpressions; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.DebugLog; /// /// Parses lines written by into structured records. diff --git a/src/EventLogExpert.UI/Services/DebugLogProjection.cs b/src/EventLogExpert.UI/DebugLog/DebugLogProjection.cs similarity index 97% rename from src/EventLogExpert.UI/Services/DebugLogProjection.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogProjection.cs index f041d09c..5dcd835f 100644 --- a/src/EventLogExpert.UI/Services/DebugLogProjection.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogProjection.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.DebugLog; public static class DebugLogProjection { diff --git a/src/EventLogExpert.UI/Services/DebugLogService.cs b/src/EventLogExpert.UI/DebugLog/DebugLogService.cs similarity index 98% rename from src/EventLogExpert.UI/Services/DebugLogService.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogService.cs index d38d8d1a..eec996d2 100644 --- a/src/EventLogExpert.UI/Services/DebugLogService.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogService.cs @@ -1,15 +1,15 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Options; +using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Settings; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.DebugLog; public sealed partial class DebugLogService : ITraceLogger, IFileLogger, IDisposable { diff --git a/src/EventLogExpert.UI/Interfaces/IFileLogger.cs b/src/EventLogExpert.UI/DebugLog/IFileLogger.cs similarity index 85% rename from src/EventLogExpert.UI/Interfaces/IFileLogger.cs rename to src/EventLogExpert.UI/DebugLog/IFileLogger.cs index 54f2f036..a73243c6 100644 --- a/src/EventLogExpert.UI/Interfaces/IFileLogger.cs +++ b/src/EventLogExpert.UI/DebugLog/IFileLogger.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.DebugLog; public interface IFileLogger { diff --git a/src/EventLogExpert.UI/Enums.cs b/src/EventLogExpert.UI/Enums.cs deleted file mode 100644 index 5ddc03b5..00000000 --- a/src/EventLogExpert.UI/Enums.cs +++ /dev/null @@ -1,125 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Runtime.Serialization; - -namespace EventLogExpert.UI; - -public enum CacheType -{ - Favorites, - Recent -} - -public enum ColumnName -{ - Level, - [EnumMember(Value = "Date and Time")] DateAndTime, - [EnumMember(Value = "Activity ID")] ActivityId, - Log, - [EnumMember(Value = "Computer Name")] ComputerName, - Source, - [EnumMember(Value = "Event ID")] EventId, - [EnumMember(Value = "Task Category")] TaskCategory, - Keywords, - [EnumMember(Value = "Process ID")] ProcessId, - [EnumMember(Value = "Thread ID")] ThreadId, - User -} - -public enum CopyType -{ - Default, - Simple, - Xml, - Full -} - -public enum DatabaseStatus -{ - NotClassified, - Ready, - UpgradeRequired, - UpgradeFailed, - UnrecognizedSchema, - ObsoleteSchema, - ClassificationFailed -} - -public enum HighlightColor -{ - None, - LightRed, - Red, - DarkRed, - LightOrange, - Orange, - DarkOrange, - LightYellow, - Yellow, - DarkYellow, - LightGreen, - Green, - DarkGreen, - LightTeal, - Teal, - DarkTeal, - LightBlue, - Blue, - DarkBlue, - LightPurple, - Purple, - DarkPurple, - LightMagenta, - Magenta, - DarkMagenta, - LightPink, - Pink, - DarkPink -} - -public enum FilterCategory -{ - [EnumMember(Value = "Event ID")] Id, - [EnumMember(Value = "Activity ID")] ActivityId, - Level, - Keywords, - Source, - [EnumMember(Value = "Task Category")] TaskCategory, - [EnumMember(Value = "Process ID")] ProcessId, - [EnumMember(Value = "Thread ID")] ThreadId, - [EnumMember(Value = "User ID")] UserId, - Description, - Xml -} - -public enum FilterEvaluator -{ - Equals, - Contains, - [EnumMember(Value = "Not Equal")] NotEqual, - [EnumMember(Value = "Not Contains")] NotContains, - [EnumMember(Value = "Multi Select")] MultiSelect -} - -public enum FilterType -{ - Basic, - Advanced, - Cached -} - -public enum OpenLogStatus -{ - Opened, - Empty, - Skipped, - Failed -} - -public enum Theme -{ - System, - Light, - Dark -} diff --git a/src/EventLogExpert.UI/EventLog/AddEventAction.cs b/src/EventLogExpert.UI/EventLog/AddEventAction.cs new file mode 100644 index 00000000..991e6a31 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/AddEventAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +public sealed record AddEventAction(ResolvedEvent NewEvent); diff --git a/src/EventLogExpert.UI/EventLog/AddEventBufferedAction.cs b/src/EventLogExpert.UI/EventLog/AddEventBufferedAction.cs new file mode 100644 index 00000000..8832fe70 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/AddEventBufferedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +public sealed record AddEventBufferedAction(IReadOnlyList UpdatedBuffer, bool IsFull); diff --git a/src/EventLogExpert.UI/EventLog/AddEventSuccessAction.cs b/src/EventLogExpert.UI/EventLog/AddEventSuccessAction.cs new file mode 100644 index 00000000..4a6def44 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/AddEventSuccessAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.EventLog; + +public sealed record AddEventSuccessAction(ImmutableDictionary ActiveLogs); diff --git a/src/EventLogExpert.UI/EventLog/CloseAllAction.cs b/src/EventLogExpert.UI/EventLog/CloseAllAction.cs new file mode 100644 index 00000000..c5f1c458 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/CloseAllAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/EventLog/CloseLogAction.cs b/src/EventLogExpert.UI/EventLog/CloseLogAction.cs new file mode 100644 index 00000000..03e2046b --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/CloseLogAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +public sealed record CloseLogAction(EventLogId LogId, string LogName); diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs b/src/EventLogExpert.UI/EventLog/Effects.cs similarity index 82% rename from src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs rename to src/EventLogExpert.UI/EventLog/Effects.cs index f2caf310..f20af790 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs +++ b/src/EventLogExpert.UI/EventLog/Effects.cs @@ -6,11 +6,12 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Resolvers; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.EventTable; -using EventLogExpert.UI.Store.FilterPane; -using EventLogExpert.UI.Store.StatusBar; +using EventLogExpert.UI.Banner; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterPane; +using EventLogExpert.UI.LogTable; +using EventLogExpert.UI.StatusBar; using Fluxor; using Microsoft.Extensions.DependencyInjection; using System.Collections.Concurrent; @@ -19,9 +20,9 @@ using System.Threading.Channels; using IDispatcher = Fluxor.IDispatcher; -namespace EventLogExpert.UI.Store.EventLog; +namespace EventLogExpert.UI.EventLog; -public sealed class EventLogEffects( +public sealed class Effects( IState eventLogState, IFilterService filterService, ITraceLogger logger, @@ -33,11 +34,12 @@ public sealed class EventLogEffects( IBannerService bannerService, IDispatcher dispatcher) : ILogReloadCoordinator { - /// Defensive timeout when awaiting a log close — both for the load task to - /// unwind (so the LoadLogAsync service scope disposes and releases its event resolver) - /// and for the watcher callbacks to drain (so per-event resolver scopes finish releasing - /// their pooled SQLite handles). 30 seconds is generous; in practice each completes in - /// milliseconds once cancellation is propagated. + /// + /// Defensive timeout when awaiting a log close — both for the load task to unwind (so the LoadLogAsync service + /// scope disposes and releases its event resolver) and for the watcher callbacks to drain (so per-event resolver + /// scopes finish releasing their pooled SQLite handles). 30 seconds is generous; in practice each completes in + /// milliseconds once cancellation is propagated. + /// public static readonly TimeSpan LogCloseTimeout = TimeSpan.FromSeconds(30); private static readonly int s_maxGlobalConcurrency = Math.Max(1, Environment.ProcessorCount - 1); @@ -51,37 +53,41 @@ public sealed class EventLogEffects( private readonly Lock _globalCtsLock = new(); private readonly ConcurrentDictionary _logCloseCompletions = new(); - /// Serializes 's XML-reload path with - /// . Both write into - /// with raw assignment; concurrent overlap on the same log id would orphan the first caller's - /// TCS (HandleCloseLog signals whichever is currently in the dict, the orphaned awaiter then - /// hits the 30s timeout). The lock ensures only one coordinator pre-registers, dispatches, - /// and awaits at a time. does NOT take this lock — it only - /// signals/removes existing entries and would deadlock against a coordinator holding the lock - /// and awaiting it. + /// + /// Serializes 's XML-reload path with + /// . Both write into with raw + /// assignment; concurrent overlap on the same log id would orphan the first caller's TCS (HandleCloseLog signals + /// whichever is currently in the dict, the orphaned awaiter then hits the 30s timeout). The lock ensures only one + /// coordinator pre-registers, dispatches, and awaits at a time. does NOT take this lock + /// — it only signals/removes existing entries and would deadlock against a coordinator holding the lock and awaiting + /// it. + /// private readonly SemaphoreSlim _logCloseCoordinatorLock = new(1, 1); private readonly ConcurrentDictionary _logCts = new(); - /// Tracks per-log load completion so can wait for - /// the in-flight service scope to dispose before draining the - /// watcher. Without this, cts.Cancel() merely requests cancellation — LoadLogAsync's - /// service scope (which owns the IEventResolver and its SQLite handles) only disposes - /// once HandleOpenLog's outer using/finally runs. + /// + /// Tracks per-log load completion so can wait for the in-flight + /// service scope to dispose before draining the watcher. Without this, cts.Cancel() merely + /// requests cancellation — LoadLogAsync's service scope (which owns the IEventResolver and its SQLite handles) only + /// disposes once HandleOpenLog's outer using/finally runs. + /// private readonly ConcurrentDictionary _logLoadCompletions = new(); private readonly ILogWatcherService _logWatcherService = logWatcherService; private readonly ITraceLogger _logger = logger; - /// Tracks which currently-open logs (by ) were loaded - /// with renderXml=true. A reload-on-transition only re-opens logs that lack XML; logs that - /// already have it are left alone. Removing or disabling an XML filter never triggers a - /// reload because the XML data is already in memory and harmless to keep. + /// + /// Tracks which currently-open logs (by ) were loaded with renderXml=true. A + /// reload-on-transition only re-opens logs that lack XML; logs that already have it are left alone. Removing or + /// disabling an XML filter never triggers a reload because the XML data is already in memory and harmless to keep. + /// private readonly ConcurrentDictionary _logsLoadedWithXml = new(); - /// Pending selection restore per log name, populated when a filter transition forces - /// a reload. Consumed by when the reloaded log finishes loading. - /// Carries both the selected record-ids and the focused record-id (if any), so reload preserves - /// the focused row in addition to the selection. + /// + /// Pending selection restore per log name, populated when a filter transition forces a reload. Consumed by + /// when the reloaded log finishes loading. Carries both the selected record-ids and + /// the focused record-id (if any), so reload preserves the focused row in addition to the selection. + /// private readonly ConcurrentDictionary _pendingSelectionRestore = new(); private readonly IEventResolverCache _resolverCache = resolverCache; private readonly IServiceScopeFactory _serviceScopeFactory = serviceScopeFactory; @@ -92,7 +98,7 @@ public sealed class EventLogEffects( private CancellationTokenSource _globalCts = new(); [EffectMethod] - public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatcher) + public Task HandleAddEvent(AddEventAction action, IDispatcher dispatcher) { // Sometimes the watcher doesn't stop firing events immediately. Let's // make sure the events being added are for a log that is still "open". @@ -104,7 +110,7 @@ public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatche _eventLogState.Value.ActiveLogs, [action.NewEvent]); - dispatcher.Dispatch(new EventLogAction.AddEventSuccess(activeLogs)); + dispatcher.Dispatch(new AddEventSuccessAction(activeLogs)); // Filter just the new event and append to the table; previous displayed // events are unchanged so a full re-filter is unnecessary. @@ -115,7 +121,7 @@ public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatche if (filteredNew.Count > 0 && activeLogs.TryGetValue(action.NewEvent.OwningLog, out var owningLog)) { - dispatcher.Dispatch(new EventTableAction.AppendTableEvents(owningLog.Id, filteredNew)); + dispatcher.Dispatch(new AppendTableEventsAction(owningLog.Id, filteredNew)); } } else @@ -129,21 +135,21 @@ public Task HandleAddEvent(EventLogAction.AddEvent action, IDispatcher dispatche var full = updatedBuffer.Count >= EventLogState.MaxNewEvents; - dispatcher.Dispatch(new EventLogAction.AddEventBuffered(updatedBuffer.AsReadOnly(), full)); + dispatcher.Dispatch(new AddEventBufferedAction(updatedBuffer.AsReadOnly(), full)); } return Task.CompletedTask; } - [EffectMethod(typeof(EventLogAction.CloseAll))] + [EffectMethod(typeof(CloseAllAction))] public async Task HandleCloseAll(IDispatcher dispatcher) { CancelAllLoads(); await _logWatcherService.RemoveAllAsync(); - dispatcher.Dispatch(new EventTableAction.CloseAll()); - dispatcher.Dispatch(new StatusBarAction.CloseAll()); + dispatcher.Dispatch(new LogTable.CloseAllAction()); + dispatcher.Dispatch(new StatusBar.CloseAllAction()); _resolverCache.ClearAll(); _xmlResolver.ClearAll(); @@ -152,7 +158,7 @@ public async Task HandleCloseAll(IDispatcher dispatcher) } [EffectMethod] - public async Task HandleCloseLog(EventLogAction.CloseLog action, IDispatcher dispatcher) + public async Task HandleCloseLog(CloseLogAction action, IDispatcher dispatcher) { try { @@ -191,7 +197,7 @@ public async Task HandleCloseLog(EventLogAction.CloseLog action, IDispatcher dis // a fresh resolve must occur instead of returning stale text from a different file. _xmlResolver.ClearXmlCacheForLog(action.LogName); - dispatcher.Dispatch(new EventTableAction.CloseLog(action.LogId)); + dispatcher.Dispatch(new LogTable.CloseLogAction(action.LogId)); if (_eventLogState.Value.ActiveLogs.IsEmpty) { @@ -211,11 +217,11 @@ public async Task HandleCloseLog(EventLogAction.CloseLog action, IDispatcher dis } [EffectMethod] - public Task HandleLoadEvents(EventLogAction.LoadEvents action, IDispatcher dispatcher) + public Task HandleLoadEvents(LoadEventsAction action, IDispatcher dispatcher) { var filteredEvents = _filterService.GetFilteredEvents(action.Events, _eventLogState.Value.AppliedFilter); - dispatcher.Dispatch(new EventTableAction.UpdateTable(action.LogData.Id, filteredEvents)); + dispatcher.Dispatch(new UpdateTableAction(action.LogData.Id, filteredEvents)); // Restore selection if this load was triggered by a filter-driven reload. // A pending entry can carry a focused row (SelectedId) without any selected @@ -239,22 +245,22 @@ public Task HandleLoadEvents(EventLogAction.LoadEvents action, IDispatcher dispa // Use SetSelectedEvents (not SelectEvents) so we can restore both // selection and the active focus row atomically. var focused = selectedRestored ?? (restored.Count > 0 ? restored[^1] : null); - dispatcher.Dispatch(new EventLogAction.SetSelectedEvents(restored, focused)); + dispatcher.Dispatch(new SetSelectedEventsAction(restored, focused)); return Task.CompletedTask; } [EffectMethod] - public Task HandleLoadEventsPartial(EventLogAction.LoadEventsPartial action, IDispatcher dispatcher) + public Task HandleLoadEventsPartial(LoadEventsPartialAction action, IDispatcher dispatcher) { var filteredEvents = _filterService.GetFilteredEvents(action.Events, _eventLogState.Value.AppliedFilter); - dispatcher.Dispatch(new EventTableAction.AppendTableEvents(action.LogData.Id, filteredEvents)); + dispatcher.Dispatch(new AppendTableEventsAction(action.LogData.Id, filteredEvents)); return Task.CompletedTask; } - [EffectMethod(typeof(EventLogAction.LoadNewEvents))] + [EffectMethod(typeof(LoadNewEventsAction))] public Task HandleLoadNewEvents(IDispatcher dispatcher) { ProcessNewEventBuffer(_eventLogState.Value, dispatcher); @@ -263,13 +269,13 @@ public Task HandleLoadNewEvents(IDispatcher dispatcher) } [EffectMethod] - public async Task HandleOpenLog(EventLogAction.OpenLog action, IDispatcher dispatcher) + public async Task HandleOpenLog(OpenLogAction action, IDispatcher dispatcher) { long startGeneration = Volatile.Read(ref _cancelGeneration); if (!_eventLogState.Value.ActiveLogs.TryGetValue(action.LogName, out var logData)) { - dispatcher.Dispatch(new StatusBarAction.SetResolverStatus($"Error: Failed to open {action.LogName}")); + dispatcher.Dispatch(new SetResolverStatusAction($"Error: Failed to open {action.LogName}")); return; } @@ -329,7 +335,7 @@ public async Task HandleOpenLog(EventLogAction.OpenLog action, IDispatcher dispa if (eventResolver is null) { - dispatcher.Dispatch(new StatusBarAction.SetResolverStatus("Error: No event resolver available")); + dispatcher.Dispatch(new SetResolverStatusAction("Error: No event resolver available")); return; } @@ -355,7 +361,7 @@ public async Task HandleOpenLog(EventLogAction.OpenLog action, IDispatcher dispa } [EffectMethod] - public Task HandleSetContinuouslyUpdate(EventLogAction.SetContinuouslyUpdate action, IDispatcher dispatcher) + public Task HandleSetContinuouslyUpdate(SetContinuouslyUpdateAction action, IDispatcher dispatcher) { if (action.ContinuouslyUpdate) { @@ -366,7 +372,7 @@ public Task HandleSetContinuouslyUpdate(EventLogAction.SetContinuouslyUpdate act } [EffectMethod] - public async Task HandleSetFilters(EventLogAction.SetFilters action, IDispatcher dispatcher) + public async Task HandleSetFilters(SetFiltersAction action, IDispatcher dispatcher) { bool newRequiresXml = action.EventFilter.RequiresXml; @@ -432,7 +438,7 @@ public async Task HandleSetFilters(EventLogAction.SetFilters action, IDispatcher foreach (var (id, name, _) in logsNeedingReload) { - dispatcher.Dispatch(new EventLogAction.CloseLog(id, name)); + dispatcher.Dispatch(new CloseLogAction(id, name)); } var timedOutLogs = new HashSet(StringComparer.Ordinal); @@ -466,7 +472,7 @@ public async Task HandleSetFilters(EventLogAction.SetFilters action, IDispatcher foreach (var (_, name, type) in logsNeedingReload) { - dispatcher.Dispatch(new EventLogAction.OpenLog(name, type)); + dispatcher.Dispatch(new OpenLogAction(name, type)); } } finally @@ -482,7 +488,7 @@ public async Task HandleSetFilters(EventLogAction.SetFilters action, IDispatcher long generation = Interlocked.Increment(ref _filterGeneration); var activeLogsSnapshot = _eventLogState.Value.ActiveLogs.Values.ToList(); - dispatcher.Dispatch(new FilterPaneAction.SetIsLoading(true)); + dispatcher.Dispatch(new SetIsLoadingAction(true)); try { @@ -541,13 +547,13 @@ public async Task HandleSetFilters(EventLogAction.SetFilters action, IDispatcher } } - dispatcher.Dispatch(new EventTableAction.UpdateDisplayedEvents(fresh)); + dispatcher.Dispatch(new UpdateDisplayedEventsAction(fresh)); } finally { if (Interlocked.Read(ref _filterGeneration) == generation) { - dispatcher.Dispatch(new FilterPaneAction.SetIsLoading(false)); + dispatcher.Dispatch(new SetIsLoadingAction(false)); } } } @@ -603,7 +609,7 @@ public async Task PrepareForDatabaseRemovalAsync(LogReopenSnapshot snapshot, Can foreach (var (id, name, _, _) in waiters) { - _dispatcher.Dispatch(new EventLogAction.CloseLog(id, name)); + _dispatcher.Dispatch(new CloseLogAction(id, name)); } // Await completion per-log so we can populate the snapshot incrementally. If a later @@ -650,7 +656,7 @@ public void ReopenAfterDatabaseRemoval(IReadOnlyList snapshot) foreach (var entry in snapshot) { - _dispatcher.Dispatch(new EventLogAction.OpenLog(entry.Name, entry.Type)); + _dispatcher.Dispatch(new OpenLogAction(entry.Name, entry.Type)); } } @@ -697,9 +703,8 @@ private static ImmutableDictionary DistributeEventsToManyL } /// - /// Awaits the producer task, suppressing all exceptions. - /// The sole purpose is to ensure the producer has fully stopped before - /// the reader is disposed. Any meaningful errors are handled by the caller. + /// Awaits the producer task, suppressing all exceptions. The sole purpose is to ensure the producer has fully + /// stopped before the reader is disposed. Any meaningful errors are handled by the caller. /// private static async Task StopProducerAsync(Task producerTask) { @@ -736,7 +741,7 @@ private void CancelAllLoads() } private async Task LoadLogAsync( - EventLogAction.OpenLog action, + OpenLogAction action, EventLogData logData, IEventResolver eventResolver, IServiceScope serviceScope, @@ -773,14 +778,14 @@ private async Task LoadLogAsync( } } - var activityId = Guid.NewGuid(); + var activityId = StatusActivityId.Create(); string? lastEvent; int failed = 0; int resolved = 0; int lastPartialIndex = 0; int timerTick = 0; - dispatcher.Dispatch(new EventTableAction.AddTable(logData)); + dispatcher.Dispatch(new AddTableAction(logData)); var channel = Channel.CreateBounded(new BoundedChannelOptions(s_maxGlobalConcurrency * 2) { @@ -793,7 +798,7 @@ private async Task LoadLogAsync( await using var timer = new Timer( _ => { - dispatcher.Dispatch(new StatusBarAction.SetEventsLoading(activityId, Volatile.Read(ref resolved), Volatile.Read(ref failed))); + dispatcher.Dispatch(new SetEventsLoadingAction(activityId, Volatile.Read(ref resolved), Volatile.Read(ref failed))); // Skip the immediate first tick (dueTime = 0) so the first partial // is dispatched after ~3 seconds of loading. @@ -811,7 +816,7 @@ private async Task LoadLogAsync( Volatile.Write(ref lastPartialIndex, events.Count); } - dispatcher.Dispatch(new EventLogAction.LoadEventsPartial(logData, delta.AsReadOnly())); + dispatcher.Dispatch(new LoadEventsPartialAction(logData, delta.AsReadOnly())); }, null, TimeSpan.Zero, @@ -916,10 +921,10 @@ await Parallel.ForEachAsync( if (_eventLogState.Value.ActiveLogs.TryGetValue(logData.Name, out var currentLog) && currentLog.Id == logData.Id) { - dispatcher.Dispatch(new EventLogAction.CloseLog(logData.Id, logData.Name)); + dispatcher.Dispatch(new CloseLogAction(logData.Id, logData.Name)); } - dispatcher.Dispatch(new StatusBarAction.ClearStatus(activityId)); + dispatcher.Dispatch(new ClearStatusAction(activityId)); return; } @@ -935,11 +940,11 @@ await Parallel.ForEachAsync( if (_eventLogState.Value.ActiveLogs.TryGetValue(logData.Name, out var currentLog) && currentLog.Id == logData.Id) { - dispatcher.Dispatch(new EventLogAction.CloseLog(logData.Id, logData.Name)); + dispatcher.Dispatch(new CloseLogAction(logData.Id, logData.Name)); } - dispatcher.Dispatch(new StatusBarAction.ClearStatus(activityId)); - dispatcher.Dispatch(new StatusBarAction.SetResolverStatus($"Error: Failed to load {action.LogName}")); + dispatcher.Dispatch(new ClearStatusAction(activityId)); + dispatcher.Dispatch(new SetResolverStatusAction($"Error: Failed to load {action.LogName}")); return; } @@ -961,23 +966,23 @@ await Parallel.ForEachAsync( _logsLoadedWithXml[logData.Id] = 0; } - dispatcher.Dispatch(new EventLogAction.LoadEvents(logData, events.AsReadOnly())); + dispatcher.Dispatch(new LoadEventsAction(logData, events.AsReadOnly())); - dispatcher.Dispatch(new StatusBarAction.SetEventsLoading(activityId, 0, 0)); + dispatcher.Dispatch(new SetEventsLoadingAction(activityId, 0, 0)); if (action.LogPathType == LogPathType.Channel) { _logWatcherService.AddLog(action.LogName, lastEvent, renderXml); } - dispatcher.Dispatch(new StatusBarAction.SetResolverStatus(string.Empty)); + dispatcher.Dispatch(new SetResolverStatusAction(string.Empty)); } private void ProcessNewEventBuffer(EventLogState state, IDispatcher dispatcher) { var activeLogs = DistributeEventsToManyLogs(state.ActiveLogs, state.NewEventBuffer); - dispatcher.Dispatch(new EventLogAction.AddEventSuccess(activeLogs)); + dispatcher.Dispatch(new AddEventSuccessAction(activeLogs)); // Group the buffered events by owning log id, filter each group, and dispatch // a single batched append so the combined-view reducer only fires once. @@ -1009,10 +1014,10 @@ private void ProcessNewEventBuffer(EventLogState state, IDispatcher dispatcher) if (batched.Count > 0) { - dispatcher.Dispatch(new EventTableAction.AppendTableEventsBatch(batched)); + dispatcher.Dispatch(new AppendTableEventsBatchAction(batched)); } - dispatcher.Dispatch(new EventLogAction.AddEventBuffered([], false)); + dispatcher.Dispatch(new AddEventBufferedAction([], false)); } } diff --git a/src/EventLogExpert.UI/Models/EventLogData.cs b/src/EventLogExpert.UI/EventLog/EventLogData.cs similarity index 94% rename from src/EventLogExpert.UI/Models/EventLogData.cs rename to src/EventLogExpert.UI/EventLog/EventLogData.cs index 0b202a3e..0c496701 100644 --- a/src/EventLogExpert.UI/Models/EventLogData.cs +++ b/src/EventLogExpert.UI/EventLog/EventLogData.cs @@ -3,8 +3,9 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Filter; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.EventLog; public sealed record EventLogData( string Name, diff --git a/src/EventLogExpert.UI/Models/EventLogId.cs b/src/EventLogExpert.UI/EventLog/EventLogId.cs similarity index 65% rename from src/EventLogExpert.UI/Models/EventLogId.cs rename to src/EventLogExpert.UI/EventLog/EventLogId.cs index 65fbfa4e..7bce3f4c 100644 --- a/src/EventLogExpert.UI/Models/EventLogId.cs +++ b/src/EventLogExpert.UI/EventLog/EventLogId.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.EventLog; public readonly record struct EventLogId(Guid Value) { diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs b/src/EventLogExpert.UI/EventLog/EventLogState.cs similarity index 92% rename from src/EventLogExpert.UI/Store/EventLog/EventLogState.cs rename to src/EventLogExpert.UI/EventLog/EventLogState.cs index bae20ee5..79d1db7c 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogState.cs +++ b/src/EventLogExpert.UI/EventLog/EventLogState.cs @@ -2,11 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.EventLog; +namespace EventLogExpert.UI.EventLog; [FeatureState] public sealed record EventLogState diff --git a/src/EventLogExpert.UI/EventLog/ILogReloadCoordinator.cs b/src/EventLogExpert.UI/EventLog/ILogReloadCoordinator.cs new file mode 100644 index 00000000..0d098d15 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/ILogReloadCoordinator.cs @@ -0,0 +1,17 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +/// +/// Bridges DatabaseService delete operations and the EventLog Effects log lifecycle. Lets the database layer ask +/// the log layer to close every active log (so SQLite handles are released) and later reopen exactly the logs that +/// closed cleanly. Implemented by EventLog Effects so close/open dispatches share the same TCS dictionaries that +/// already track per-log load and close completion. +/// +public interface ILogReloadCoordinator +{ + Task PrepareForDatabaseRemovalAsync(LogReopenSnapshot snapshot, CancellationToken cancellationToken = default); + + void ReopenAfterDatabaseRemoval(IReadOnlyList snapshot); +} diff --git a/src/EventLogExpert.UI/EventLog/ILogWatcherService.cs b/src/EventLogExpert.UI/EventLog/ILogWatcherService.cs new file mode 100644 index 00000000..5bde8f4a --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/ILogWatcherService.cs @@ -0,0 +1,13 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +public interface ILogWatcherService +{ + void AddLog(string logName, string? bookmark, bool renderXml = false); + + Task RemoveAllAsync(); + + Task RemoveLogAsync(string logName); +} diff --git a/src/EventLogExpert.UI/EventLog/LoadEventsAction.cs b/src/EventLogExpert.UI/EventLog/LoadEventsAction.cs new file mode 100644 index 00000000..2fbab64d --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/LoadEventsAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +public sealed record LoadEventsAction(EventLogData LogData, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/EventLog/LoadEventsPartialAction.cs b/src/EventLogExpert.UI/EventLog/LoadEventsPartialAction.cs new file mode 100644 index 00000000..f472be13 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/LoadEventsPartialAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +public sealed record LoadEventsPartialAction(EventLogData LogData, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/EventLog/LoadNewEventsAction.cs b/src/EventLogExpert.UI/EventLog/LoadNewEventsAction.cs new file mode 100644 index 00000000..2eb7ba9e --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/LoadNewEventsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +internal sealed record LoadNewEventsAction; diff --git a/src/EventLogExpert.UI/EventLog/LogReopenInfo.cs b/src/EventLogExpert.UI/EventLog/LogReopenInfo.cs new file mode 100644 index 00000000..baa8cc81 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/LogReopenInfo.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Channels; + +namespace EventLogExpert.UI.EventLog; + +public sealed record LogReopenInfo(string Name, LogPathType Type); diff --git a/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs b/src/EventLogExpert.UI/EventLog/LogReopenSnapshot.cs similarity index 51% rename from src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs rename to src/EventLogExpert.UI/EventLog/LogReopenSnapshot.cs index 36f62105..0684d338 100644 --- a/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs +++ b/src/EventLogExpert.UI/EventLog/LogReopenSnapshot.cs @@ -1,24 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.Eventing.Common.Channels; - -namespace EventLogExpert.UI.Store.EventLog; - -/// -/// Bridges DatabaseService delete operations and the EventLogEffects log lifecycle. Lets the database layer ask -/// the log layer to close every active log (so SQLite handles are released) and later reopen exactly the logs that -/// closed cleanly. Implemented by EventLogEffects so close/open dispatches share the same TCS dictionaries that -/// already track per-log load and close completion. -/// -public interface ILogReloadCoordinator -{ - Task PrepareForDatabaseRemovalAsync(LogReopenSnapshot snapshot, CancellationToken cancellationToken = default); - - void ReopenAfterDatabaseRemoval(IReadOnlyList snapshot); -} - -public sealed record LogReopenInfo(string Name, LogPathType Type); +namespace EventLogExpert.UI.EventLog; /// /// Mutable container that the coordinator populates as each active log finishes closing. Callers pass an empty diff --git a/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs b/src/EventLogExpert.UI/EventLog/LogWatcherService.cs similarity index 95% rename from src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs rename to src/EventLogExpert.UI/EventLog/LogWatcherService.cs index 82274ae5..d881a571 100644 --- a/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs +++ b/src/EventLogExpert.UI/EventLog/LogWatcherService.cs @@ -7,16 +7,7 @@ using Fluxor; using Microsoft.Extensions.DependencyInjection; -namespace EventLogExpert.UI.Store.EventLog; - -public interface ILogWatcherService -{ - void AddLog(string logName, string? bookmark, bool renderXml = false); - - Task RemoveAllAsync(); - - Task RemoveLogAsync(string logName); -} +namespace EventLogExpert.UI.EventLog; public sealed class LogWatcherService : ILogWatcherService { @@ -190,7 +181,7 @@ private void StartWatching(string logName) return; } - _dispatcher.Dispatch(new EventLogAction.AddEvent(eventResolver.ResolveEvent(eventArgs))); + _dispatcher.Dispatch(new AddEventAction(eventResolver.ResolveEvent(eventArgs))); }; // When the watcher is enabled, it reads all the events since the diff --git a/src/EventLogExpert.UI/EventLog/OpenLogAction.cs b/src/EventLogExpert.UI/EventLog/OpenLogAction.cs new file mode 100644 index 00000000..beddbc93 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/OpenLogAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Channels; + +namespace EventLogExpert.UI.EventLog; + +public sealed record OpenLogAction(string LogName, LogPathType LogPathType, CancellationToken Token = default); diff --git a/src/EventLogExpert.UI/EventLog/OpenLogStatus.cs b/src/EventLogExpert.UI/EventLog/OpenLogStatus.cs new file mode 100644 index 00000000..f0d8310b --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/OpenLogStatus.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +public enum OpenLogStatus +{ + Opened, + Empty, + Skipped, + Failed +} diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs b/src/EventLogExpert.UI/EventLog/Reducers.cs similarity index 94% rename from src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs rename to src/EventLogExpert.UI/EventLog/Reducers.cs index fa9e7ed4..de791fd0 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs +++ b/src/EventLogExpert.UI/EventLog/Reducers.cs @@ -3,23 +3,23 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.EventLog; +namespace EventLogExpert.UI.EventLog; -public sealed class EventLogReducers +public sealed class Reducers { [ReducerMethod] - public static EventLogState ReduceAddEventBuffered(EventLogState state, EventLogAction.AddEventBuffered action) => + public static EventLogState ReduceAddEventBuffered(EventLogState state, AddEventBufferedAction action) => state with { NewEventBuffer = action.UpdatedBuffer, NewEventBufferIsFull = action.IsFull }; [ReducerMethod] - public static EventLogState ReduceAddEventSuccess(EventLogState state, EventLogAction.AddEventSuccess action) => + public static EventLogState ReduceAddEventSuccess(EventLogState state, AddEventSuccessAction action) => state with { ActiveLogs = action.ActiveLogs }; - [ReducerMethod(typeof(EventLogAction.CloseAll))] + [ReducerMethod(typeof(CloseAllAction))] public static EventLogState ReduceCloseAll(EventLogState state) => state with { @@ -31,7 +31,7 @@ state with }; [ReducerMethod] - public static EventLogState ReduceCloseLog(EventLogState state, EventLogAction.CloseLog action) + public static EventLogState ReduceCloseLog(EventLogState state, CloseLogAction action) { var newEventBuffer = state.NewEventBuffer .Where(e => e.OwningLog != action.LogName) @@ -63,7 +63,7 @@ state.SelectedEvent is not null && } [ReducerMethod] - public static EventLogState ReduceLoadEvents(EventLogState state, EventLogAction.LoadEvents action) + public static EventLogState ReduceLoadEvents(EventLogState state, LoadEventsAction action) { if (!state.ActiveLogs.TryGetValue(action.LogData.Name, out var existing) || existing.Id != action.LogData.Id) @@ -75,7 +75,7 @@ public static EventLogState ReduceLoadEvents(EventLogState state, EventLogAction } [ReducerMethod] - public static EventLogState ReduceLoadEventsPartial(EventLogState state, EventLogAction.LoadEventsPartial action) + public static EventLogState ReduceLoadEventsPartial(EventLogState state, LoadEventsPartialAction action) { if (!state.ActiveLogs.TryGetValue(action.LogData.Name, out var existingLog) || existingLog.Id != action.LogData.Id) @@ -96,7 +96,7 @@ public static EventLogState ReduceLoadEventsPartial(EventLogState state, EventLo } [ReducerMethod] - public static EventLogState ReduceOpenLog(EventLogState state, EventLogAction.OpenLog action) => + public static EventLogState ReduceOpenLog(EventLogState state, OpenLogAction action) => // Idempotent: re-opening an already-active log is a no-op so callers (menu, drag/drop, command line, // SettingsModal.ReloadOpenLogs, effects) don't need to coordinate to avoid ImmutableDictionary.Add throwing. // TODO: HandleOpenLog effect still runs for every dispatched OpenLog action, so a duplicate dispatch can @@ -110,7 +110,7 @@ public static EventLogState ReduceOpenLog(EventLogState state, EventLogAction.Op }; [ReducerMethod] - public static EventLogState ReduceSelectEvent(EventLogState state, EventLogAction.SelectEvent action) + public static EventLogState ReduceSelectEvent(EventLogState state, SelectEventAction action) { // Reference equality keeps selection consistent with EventTable's // ReferenceEqualityComparer-based selection set. Value equality on @@ -151,7 +151,7 @@ public static EventLogState ReduceSelectEvent(EventLogState state, EventLogActio } [ReducerMethod] - public static EventLogState ReduceSelectEvents(EventLogState state, EventLogAction.SelectEvents action) + public static EventLogState ReduceSelectEvents(EventLogState state, SelectEventsAction action) { // Reference-identity dedupe only: prevents adding the same instance // twice, but intentionally allows distinct value-equal instances @@ -222,11 +222,11 @@ public static EventLogState ReduceSelectEvents(EventLogState state, EventLogActi [ReducerMethod] public static EventLogState ReduceSetContinuouslyUpdate( EventLogState state, - EventLogAction.SetContinuouslyUpdate action) => + SetContinuouslyUpdateAction action) => state with { ContinuouslyUpdate = action.ContinuouslyUpdate }; [ReducerMethod] - public static EventLogState ReduceSetFilters(EventLogState state, EventLogAction.SetFilters action) + public static EventLogState ReduceSetFilters(EventLogState state, SetFiltersAction action) { if (!FilterMethods.HasFilteringChanged(action.EventFilter, state.AppliedFilter)) { @@ -237,7 +237,7 @@ public static EventLogState ReduceSetFilters(EventLogState state, EventLogAction } [ReducerMethod] - public static EventLogState ReduceSetSelectedEvents(EventLogState state, EventLogAction.SetSelectedEvents action) + public static EventLogState ReduceSetSelectedEvents(EventLogState state, SetSelectedEventsAction action) { // Order-preserving distinct by reference identity. The caller (typically // EventTable) is responsible for ordering events according to the current diff --git a/src/EventLogExpert.UI/EventLog/SelectEventAction.cs b/src/EventLogExpert.UI/EventLog/SelectEventAction.cs new file mode 100644 index 00000000..8a7ee15c --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/SelectEventAction.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +public sealed record SelectEventAction( + ResolvedEvent SelectedEvent, + bool IsMultiSelect = false, + bool ShouldStaySelected = false); diff --git a/src/EventLogExpert.UI/EventLog/SelectEventsAction.cs b/src/EventLogExpert.UI/EventLog/SelectEventsAction.cs new file mode 100644 index 00000000..7d2b1235 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/SelectEventsAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +public sealed record SelectEventsAction(IReadOnlyCollection SelectedEvents); diff --git a/src/EventLogExpert.UI/EventLog/SetContinuouslyUpdateAction.cs b/src/EventLogExpert.UI/EventLog/SetContinuouslyUpdateAction.cs new file mode 100644 index 00000000..a1f4a709 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/SetContinuouslyUpdateAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.EventLog; + +public sealed record SetContinuouslyUpdateAction(bool ContinuouslyUpdate); diff --git a/src/EventLogExpert.UI/EventLog/SetFiltersAction.cs b/src/EventLogExpert.UI/EventLog/SetFiltersAction.cs new file mode 100644 index 00000000..04bc8bb0 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/SetFiltersAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.EventLog; + +public sealed record SetFiltersAction(EventFilter EventFilter); diff --git a/src/EventLogExpert.UI/EventLog/SetSelectedEventsAction.cs b/src/EventLogExpert.UI/EventLog/SetSelectedEventsAction.cs new file mode 100644 index 00000000..a78686c5 --- /dev/null +++ b/src/EventLogExpert.UI/EventLog/SetSelectedEventsAction.cs @@ -0,0 +1,24 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.EventLog; + +/// +/// Replaces the entire selection with the supplied events, preserving input order and de-duplicating by reference +/// identity, and atomically updates the focused (selected) event. Use this for range selection (Shift+Click, +/// Shift+Arrow), Select All (Ctrl+A), clear (Escape), and any selection update where the caller already knows both the +/// new selection and the new focus. +/// +/// +/// The new selection list, in the order that should be preserved (typically the current +/// table's sort order). +/// +/// +/// The new focused event, or null to clear focus. Does not need to be a member of +/// . +/// +public sealed record SetSelectedEventsAction( + IReadOnlyCollection SelectedEvents, + ResolvedEvent? SelectedEvent); diff --git a/src/EventLogExpert.UI/EventLogExpert.UI.csproj b/src/EventLogExpert.UI/EventLogExpert.UI.csproj index 1f97ddb6..f7283dd9 100644 --- a/src/EventLogExpert.UI/EventLogExpert.UI.csproj +++ b/src/EventLogExpert.UI/EventLogExpert.UI.csproj @@ -13,4 +13,9 @@ + + + + + diff --git a/src/EventLogExpert.UI/Models/BasicFilter.cs b/src/EventLogExpert.UI/Filter/BasicFilter.cs similarity index 88% rename from src/EventLogExpert.UI/Models/BasicFilter.cs rename to src/EventLogExpert.UI/Filter/BasicFilter.cs index 7e3a820a..542e3cab 100644 --- a/src/EventLogExpert.UI/Models/BasicFilter.cs +++ b/src/EventLogExpert.UI/Filter/BasicFilter.cs @@ -3,7 +3,7 @@ using System.Collections.Immutable; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; public sealed record BasicFilter(FilterData Comparison, ImmutableList SubFilters) { diff --git a/src/EventLogExpert.UI/Models/CompiledFilter.cs b/src/EventLogExpert.UI/Filter/CompiledFilter.cs similarity index 94% rename from src/EventLogExpert.UI/Models/CompiledFilter.cs rename to src/EventLogExpert.UI/Filter/CompiledFilter.cs index a61b5aaf..e1ec8dc5 100644 --- a/src/EventLogExpert.UI/Models/CompiledFilter.cs +++ b/src/EventLogExpert.UI/Filter/CompiledFilter.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.Common.Events; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; /// /// Immutable compiled artifact: the runnable predicate plus the cached XML-access flag used to decide whether diff --git a/src/EventLogExpert.UI/Models/FilterDateModel.cs b/src/EventLogExpert.UI/Filter/DateFilter.cs similarity index 75% rename from src/EventLogExpert.UI/Models/FilterDateModel.cs rename to src/EventLogExpert.UI/Filter/DateFilter.cs index cab750d5..7443ee6b 100644 --- a/src/EventLogExpert.UI/Models/FilterDateModel.cs +++ b/src/EventLogExpert.UI/Filter/DateFilter.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; -public sealed record FilterDateModel +public sealed record DateFilter { public DateTime? After { get; set; } diff --git a/src/EventLogExpert.UI/DateRangeDefaults.cs b/src/EventLogExpert.UI/Filter/DateRangeDefaults.cs similarity index 95% rename from src/EventLogExpert.UI/DateRangeDefaults.cs rename to src/EventLogExpert.UI/Filter/DateRangeDefaults.cs index 534679ee..3638dcb3 100644 --- a/src/EventLogExpert.UI/DateRangeDefaults.cs +++ b/src/EventLogExpert.UI/Filter/DateRangeDefaults.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.EventLog; -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Filter; /// /// Computes the default initial bounds for the date-range filter from the active logs. Uses ENVELOPE semantics: @@ -18,7 +18,7 @@ namespace EventLogExpert.UI; /// (After floored, Before ceilinged) so callers get a deterministic, non-empty /// range. /// -public static class DateRangeDefaults +internal static class DateRangeDefaults { private static readonly long s_ticksPerHour = TimeSpan.FromHours(1).Ticks; diff --git a/src/EventLogExpert.UI/Models/EventFilter.cs b/src/EventLogExpert.UI/Filter/EventFilter.cs similarity index 69% rename from src/EventLogExpert.UI/Models/EventFilter.cs rename to src/EventLogExpert.UI/Filter/EventFilter.cs index 18534d5f..8407e6bb 100644 --- a/src/EventLogExpert.UI/Models/EventFilter.cs +++ b/src/EventLogExpert.UI/Filter/EventFilter.cs @@ -3,14 +3,14 @@ using System.Collections.Immutable; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; -/// Snapshot of fields that affect the filtered event set. +/// Snapshot of fields that affect the filtered event set. public readonly record struct FilterSnapshot(string Value, bool IsExcluded); public readonly record struct EventFilter { - public EventFilter(FilterDateModel? dateFilter, ImmutableList filters) + public EventFilter(DateFilter? dateFilter, ImmutableList filters) { DateFilter = dateFilter; Filters = filters; @@ -18,17 +18,20 @@ public EventFilter(FilterDateModel? dateFilter, ImmutableList filte RequiresXml = ComputeRequiresXml(Filters); } - public FilterDateModel? DateFilter { get; } + public DateFilter? DateFilter { get; } - public ImmutableList Filters { get; } + public ImmutableList Filters { get; } /// Construction-time snapshot used by . public ImmutableArray Snapshots { get; } - /// True when any filter or sub-filter references . + /// + /// True when any filter or sub-filter references + /// . + /// public bool RequiresXml { get; } - private static bool ComputeRequiresXml(ImmutableList filters) + private static bool ComputeRequiresXml(ImmutableList filters) { if (filters.IsEmpty) { return false; } @@ -40,7 +43,7 @@ private static bool ComputeRequiresXml(ImmutableList filters) return false; } - private static ImmutableArray ComputeSnapshots(ImmutableList filters) + private static ImmutableArray ComputeSnapshots(ImmutableList filters) { if (filters.Count == 0) { return ImmutableArray.Empty; } diff --git a/src/EventLogExpert.UI/Filter/FilterCategory.cs b/src/EventLogExpert.UI/Filter/FilterCategory.cs new file mode 100644 index 00000000..d7b367bc --- /dev/null +++ b/src/EventLogExpert.UI/Filter/FilterCategory.cs @@ -0,0 +1,21 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace EventLogExpert.UI.Filter; + +public enum FilterCategory +{ + [EnumMember(Value = "Event ID")] Id, + [EnumMember(Value = "Activity ID")] ActivityId, + Level, + Keywords, + Source, + [EnumMember(Value = "Task Category")] TaskCategory, + [EnumMember(Value = "Process ID")] ProcessId, + [EnumMember(Value = "Thread ID")] ThreadId, + [EnumMember(Value = "User ID")] UserId, + Description, + Xml +} diff --git a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs b/src/EventLogExpert.UI/Filter/FilterCategoryItemsCache.cs similarity index 83% rename from src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs rename to src/EventLogExpert.UI/Filter/FilterCategoryItemsCache.cs index 8d7d1efe..25e6b88a 100644 --- a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs +++ b/src/EventLogExpert.UI/Filter/FilterCategoryItemsCache.cs @@ -2,17 +2,17 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.EventLog; using System.Collections.Concurrent; using System.Collections.Immutable; using System.Runtime.CompilerServices; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Filter; /// -/// Caches the distinct, sorted list of category values across all active logs, keyed by the -/// snapshot reference. Each snapshot change -/// produces a new key, so cache entries auto-evict via . +/// Caches the distinct, sorted list of category values across all active logs, keyed by the +/// snapshot reference. Each snapshot change produces a new key, so +/// cache entries auto-evict via . /// public static class FilterCategoryItemsCache { diff --git a/src/EventLogExpert.UI/Services/FilterCompiler.cs b/src/EventLogExpert.UI/Filter/FilterCompiler.cs similarity index 97% rename from src/EventLogExpert.UI/Services/FilterCompiler.cs rename to src/EventLogExpert.UI/Filter/FilterCompiler.cs index 8856de93..f75cc6ae 100644 --- a/src/EventLogExpert.UI/Services/FilterCompiler.cs +++ b/src/EventLogExpert.UI/Filter/FilterCompiler.cs @@ -2,12 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; using System.Diagnostics.CodeAnalysis; using System.Linq.Dynamic.Core; using System.Linq.Expressions; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Filter; /// Single source of truth for compiling a Dynamic LINQ expression string into a . public static class FilterCompiler diff --git a/src/EventLogExpert.UI/Models/FilterData.cs b/src/EventLogExpert.UI/Filter/FilterData.cs similarity index 69% rename from src/EventLogExpert.UI/Models/FilterData.cs rename to src/EventLogExpert.UI/Filter/FilterData.cs index 6c30bc70..04fb3122 100644 --- a/src/EventLogExpert.UI/Models/FilterData.cs +++ b/src/EventLogExpert.UI/Filter/FilterData.cs @@ -3,11 +3,9 @@ using System.Collections.Immutable; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; -/// -/// Immutable representation of a single Basic-filter criterion (one row of the editor). -/// +/// Immutable representation of a single Basic-filter criterion (one row of the editor). public sealed record FilterData { public FilterCategory Category { get; init; } @@ -19,8 +17,8 @@ public sealed record FilterData public ImmutableList Values { get; init; } = []; /// - /// Returns a copy with the new and Value/Values cleared, - /// since the available value space changes when the category changes. + /// Returns a copy with the new and Value/Values cleared, since the available value + /// space changes when the category changes. /// public FilterData WithCategory(FilterCategory category) => this with { Category = category, Value = null, Values = [] }; diff --git a/src/EventLogExpert.UI/Models/FilterDataDraft.cs b/src/EventLogExpert.UI/Filter/FilterDataDraft.cs similarity index 97% rename from src/EventLogExpert.UI/Models/FilterDataDraft.cs rename to src/EventLogExpert.UI/Filter/FilterDataDraft.cs index 9115bda9..fc699231 100644 --- a/src/EventLogExpert.UI/Models/FilterDataDraft.cs +++ b/src/EventLogExpert.UI/Filter/FilterDataDraft.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; /// /// Mutable editor mirror of used by the Basic-filter UI. Exists because Blazor two-way diff --git a/src/EventLogExpert.UI/Models/FilterDraftModel.cs b/src/EventLogExpert.UI/Filter/FilterDraft.cs similarity index 85% rename from src/EventLogExpert.UI/Models/FilterDraftModel.cs rename to src/EventLogExpert.UI/Filter/FilterDraft.cs index c62196e0..298f4248 100644 --- a/src/EventLogExpert.UI/Models/FilterDraftModel.cs +++ b/src/EventLogExpert.UI/Filter/FilterDraft.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; -public sealed class FilterDraftModel +public sealed class FilterDraft { public HighlightColor Color { get; set; } = HighlightColor.None; @@ -22,15 +22,15 @@ public sealed class FilterDraftModel public List SubFilters { get; set; } = []; /// - /// Hydrates Basic structure from when present so Basic re-edit reopens + /// Hydrates Basic structure from when present so Basic re-edit reopens /// with the original comparison + sub-filters; otherwise opens empty Basic fields with just - /// populated. + /// populated. /// - public static FilterDraftModel FromFilterModel(FilterModel filter) + public static FilterDraft FromSavedFilter(SavedFilter filter) { var basicFilter = filter.FilterType == FilterType.Basic ? filter.BasicFilter : null; - return new FilterDraftModel + return new FilterDraft { Id = filter.Id, Color = filter.Color, diff --git a/src/EventLogExpert.UI/Filter/FilterEvaluator.cs b/src/EventLogExpert.UI/Filter/FilterEvaluator.cs new file mode 100644 index 00000000..b3cf5d1b --- /dev/null +++ b/src/EventLogExpert.UI/Filter/FilterEvaluator.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace EventLogExpert.UI.Filter; + +public enum FilterEvaluator +{ + Equals, + Contains, + [EnumMember(Value = "Not Equal")] NotEqual, + [EnumMember(Value = "Not Contains")] NotContains, + [EnumMember(Value = "Multi Select")] MultiSelect +} diff --git a/src/EventLogExpert.UI/Models/FilterGroupData.cs b/src/EventLogExpert.UI/Filter/FilterGroupData.cs similarity index 53% rename from src/EventLogExpert.UI/Models/FilterGroupData.cs rename to src/EventLogExpert.UI/Filter/FilterGroupData.cs index 97292136..b003d8a9 100644 --- a/src/EventLogExpert.UI/Models/FilterGroupData.cs +++ b/src/EventLogExpert.UI/Filter/FilterGroupData.cs @@ -1,11 +1,11 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; public sealed record FilterGroupData { public Dictionary ChildGroup { get; init; } = []; - public List FilterGroups { get; init; } = []; + public List FilterGroups { get; init; } = []; } diff --git a/src/EventLogExpert.UI/Models/FilterGroupId.cs b/src/EventLogExpert.UI/Filter/FilterGroupId.cs similarity index 66% rename from src/EventLogExpert.UI/Models/FilterGroupId.cs rename to src/EventLogExpert.UI/Filter/FilterGroupId.cs index ab0a0b9b..34da664b 100644 --- a/src/EventLogExpert.UI/Models/FilterGroupId.cs +++ b/src/EventLogExpert.UI/Filter/FilterGroupId.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; public readonly record struct FilterGroupId(Guid Value) { diff --git a/src/EventLogExpert.UI/Models/FilterId.cs b/src/EventLogExpert.UI/Filter/FilterId.cs similarity index 65% rename from src/EventLogExpert.UI/Models/FilterId.cs rename to src/EventLogExpert.UI/Filter/FilterId.cs index cbcf35cd..0ddeff77 100644 --- a/src/EventLogExpert.UI/Models/FilterId.cs +++ b/src/EventLogExpert.UI/Filter/FilterId.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; public readonly record struct FilterId(Guid Value) { diff --git a/src/EventLogExpert.UI/FilterMethods.cs b/src/EventLogExpert.UI/Filter/FilterMethods.cs similarity index 97% rename from src/EventLogExpert.UI/FilterMethods.cs rename to src/EventLogExpert.UI/Filter/FilterMethods.cs index 23139d35..fc48e991 100644 --- a/src/EventLogExpert.UI/FilterMethods.cs +++ b/src/EventLogExpert.UI/Filter/FilterMethods.cs @@ -2,11 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.LogTable; -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Filter; -public static class FilterMethods +internal static class FilterMethods { private static readonly Comparison s_ascByLevel = (a, b) => WithTieBreaker(string.Compare(a.Level, b.Level, StringComparison.Ordinal), a, b); @@ -53,7 +53,7 @@ public static class FilterMethods public static Dictionary AddFilterGroup( this Dictionary group, string[] groupNames, - FilterGroupModel data) + SavedFilterGroup data) { var root = groupNames.Length <= 1 ? string.Empty : groupNames.First(); groupNames = groupNames.Skip(1).ToArray(); @@ -258,7 +258,7 @@ private static int WithTieBreaker(int primaryResult, ResolvedEvent a, ResolvedEv extension(ResolvedEvent? @event) { - public bool Filter(IEnumerable filters) + public bool Filter(IEnumerable filters) { if (@event is null) { return false; } @@ -279,7 +279,7 @@ public bool Filter(IEnumerable filters) return isEmpty || isFiltered; } - public ResolvedEvent? FilterByDate(FilterDateModel? dateFilter) + public ResolvedEvent? FilterByDate(DateFilter? dateFilter) { if (@event is null) { return null; } diff --git a/src/EventLogExpert.UI/Services/FilterService.cs b/src/EventLogExpert.UI/Filter/FilterService.cs similarity index 99% rename from src/EventLogExpert.UI/Services/FilterService.cs rename to src/EventLogExpert.UI/Filter/FilterService.cs index 6609809b..424b30e2 100644 --- a/src/EventLogExpert.UI/Services/FilterService.cs +++ b/src/EventLogExpert.UI/Filter/FilterService.cs @@ -2,13 +2,12 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.EventLog; using System.Linq.Dynamic.Core; using System.Runtime.ExceptionServices; using System.Text; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Filter; public sealed class FilterService : IFilterService { diff --git a/src/EventLogExpert.UI/Filter/FilterType.cs b/src/EventLogExpert.UI/Filter/FilterType.cs new file mode 100644 index 00000000..860bb39a --- /dev/null +++ b/src/EventLogExpert.UI/Filter/FilterType.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Filter; + +public enum FilterType +{ + Basic, + Advanced, + Cached +} diff --git a/src/EventLogExpert.UI/Filter/HighlightColor.cs b/src/EventLogExpert.UI/Filter/HighlightColor.cs new file mode 100644 index 00000000..c97adead --- /dev/null +++ b/src/EventLogExpert.UI/Filter/HighlightColor.cs @@ -0,0 +1,36 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Filter; + +public enum HighlightColor +{ + None, + LightRed, + Red, + DarkRed, + LightOrange, + Orange, + DarkOrange, + LightYellow, + Yellow, + DarkYellow, + LightGreen, + Green, + DarkGreen, + LightTeal, + Teal, + DarkTeal, + LightBlue, + Blue, + DarkBlue, + LightPurple, + Purple, + DarkPurple, + LightMagenta, + Magenta, + DarkMagenta, + LightPink, + Pink, + DarkPink +} diff --git a/src/EventLogExpert.UI/Interfaces/IFilterService.cs b/src/EventLogExpert.UI/Filter/IFilterService.cs similarity index 88% rename from src/EventLogExpert.UI/Interfaces/IFilterService.cs rename to src/EventLogExpert.UI/Filter/IFilterService.cs index baa8cd70..33f1bcdc 100644 --- a/src/EventLogExpert.UI/Interfaces/IFilterService.cs +++ b/src/EventLogExpert.UI/Filter/IFilterService.cs @@ -2,9 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.EventLog; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Filter; public interface IFilterService { diff --git a/src/EventLogExpert.UI/Models/FilterModel.cs b/src/EventLogExpert.UI/Filter/SavedFilter.cs similarity index 80% rename from src/EventLogExpert.UI/Models/FilterModel.cs rename to src/EventLogExpert.UI/Filter/SavedFilter.cs index 0f4b02e0..814335a8 100644 --- a/src/EventLogExpert.UI/Models/FilterModel.cs +++ b/src/EventLogExpert.UI/Filter/SavedFilter.cs @@ -1,21 +1,20 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; using System.Text.Json.Serialization; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; /// /// Immutable saved filter. Carries together with the pre-compiled /// predicate so consumers never have to recompile to evaluate. Basic filters additionally /// retain for round-trip re-edit. /// -[JsonConverter(typeof(FilterModelJsonConverter))] -public sealed record FilterModel +[JsonConverter(typeof(SavedFilterJsonConverter))] +public sealed record SavedFilter { - /// Placeholder where a default-valued is required. - public static readonly FilterModel Empty = new() { ComparisonText = string.Empty, Compiled = null }; + /// Placeholder where a default-valued is required. + public static readonly SavedFilter Empty = new() { ComparisonText = string.Empty, Compiled = null }; [JsonIgnore] public FilterId Id { get; init; } = FilterId.Create(); @@ -33,8 +32,8 @@ public sealed record FilterModel public required CompiledFilter? Compiled { get; init; } /// - /// Structured form of Basic filters; persisted so re-edit reopens the original comparison + sub-filter - /// structure. null for Advanced and Cached filters. + /// Structured form of Basic filters; persisted so re-edit reopens the original comparison + sub-filter structure. + /// null for Advanced and Cached filters. /// public BasicFilter? BasicFilter { get; init; } @@ -46,10 +45,10 @@ public sealed record FilterModel public bool IsExcluded { get; init; } /// - /// Compiles and returns a populated , or null + /// Compiles and returns a populated , or null /// if the expression fails to parse. /// - public static FilterModel? TryCreate( + public static SavedFilter? TryCreate( string comparisonText, FilterType filterType = FilterType.Advanced, BasicFilter? basicFilter = null, @@ -60,7 +59,7 @@ public sealed record FilterModel { if (!FilterCompiler.TryCompile(comparisonText, out var compiled, out _)) { return null; } - return new FilterModel + return new SavedFilter { Id = id ?? FilterId.Create(), Color = color, diff --git a/src/EventLogExpert.UI/Models/FilterGroupModel.cs b/src/EventLogExpert.UI/Filter/SavedFilterGroup.cs similarity index 67% rename from src/EventLogExpert.UI/Models/FilterGroupModel.cs rename to src/EventLogExpert.UI/Filter/SavedFilterGroup.cs index 8e40e12d..d9298f0d 100644 --- a/src/EventLogExpert.UI/Models/FilterGroupModel.cs +++ b/src/EventLogExpert.UI/Filter/SavedFilterGroup.cs @@ -1,11 +1,11 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using System.Text.Json.Serialization; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; -public sealed record FilterGroupModel +public sealed record SavedFilterGroup { [JsonIgnore] public FilterGroupId Id { get; } = FilterGroupId.Create(); @@ -15,7 +15,7 @@ public sealed record FilterGroupModel [JsonIgnore] public string DisplayName => Name.Split('\\').Last(); - public IReadOnlyList Filters { get; init; } = []; + public IReadOnlyList Filters { get; init; } = []; [JsonIgnore] public bool IsEditing { get; init; } diff --git a/src/EventLogExpert.UI/Models/FilterModelJsonConverter.cs b/src/EventLogExpert.UI/Filter/SavedFilterJsonConverter.cs similarity index 91% rename from src/EventLogExpert.UI/Models/FilterModelJsonConverter.cs rename to src/EventLogExpert.UI/Filter/SavedFilterJsonConverter.cs index b4524fe5..db65d259 100644 --- a/src/EventLogExpert.UI/Models/FilterModelJsonConverter.cs +++ b/src/EventLogExpert.UI/Filter/SavedFilterJsonConverter.cs @@ -1,27 +1,26 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; using System.Diagnostics; using System.Text.Json; using System.Text.Json.Serialization; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; /// -/// Reads and writes JSON. Accepts both the legacy persisted shape ( +/// Reads and writes JSON. Accepts both the legacy persisted shape ( /// { "Color": n, "Comparison": { "Value": "..." }, "IsExcluded": b }) and the new shape ( /// { "Color": n, "ComparisonText": "...", "IsExcluded": b, "FilterType": "Advanced", "BasicFilter": ... }). /// Always writes the new shape. /// /// When a persisted ComparisonText fails to compile, the loaded filter retains the text and -/// but has Compiled == null and IsEnabled == false so it is +/// but has Compiled == null and IsEnabled == false so it is /// visible in the UI for the user to repair without forcing application start to fail. /// /// -public sealed class FilterModelJsonConverter : JsonConverter +internal sealed class SavedFilterJsonConverter : JsonConverter { - public override FilterModel Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override SavedFilter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) { @@ -93,7 +92,7 @@ public override FilterModel Read(ref Utf8JsonReader reader, Type typeToConvert, if (filterType == FilterType.Basic && basicFilter is null) { Trace.TraceWarning( - "FilterModelJsonConverter: persisted Basic filter has no BasicFilter; degrading to Advanced. Text='{0}'", + "SavedFilterJsonConverter: persisted Basic filter has no BasicFilter; degrading to Advanced. Text='{0}'", text); filterType = FilterType.Advanced; @@ -111,14 +110,14 @@ public override FilterModel Read(ref Utf8JsonReader reader, Type typeToConvert, if (!string.IsNullOrEmpty(text) && !FilterCompiler.TryCompile(text, out compiled, out string? error)) { Trace.TraceWarning( - "FilterModelJsonConverter: failed to compile persisted filter expression. Text='{0}', Error='{1}'", + "SavedFilterJsonConverter: failed to compile persisted filter expression. Text='{0}', Error='{1}'", text, error); compileFailed = true; } - return new FilterModel + return new SavedFilter { Color = color, ComparisonText = text, @@ -130,7 +129,7 @@ public override FilterModel Read(ref Utf8JsonReader reader, Type typeToConvert, }; } - public override void Write(Utf8JsonWriter writer, FilterModel value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SavedFilter value, JsonSerializerOptions options) { writer.WriteStartObject(); writer.WriteNumber("Color", (int)value.Color); diff --git a/src/EventLogExpert.UI/Models/SubFilter.cs b/src/EventLogExpert.UI/Filter/SubFilter.cs similarity index 80% rename from src/EventLogExpert.UI/Models/SubFilter.cs rename to src/EventLogExpert.UI/Filter/SubFilter.cs index cf2cbc5f..bcd10aa1 100644 --- a/src/EventLogExpert.UI/Models/SubFilter.cs +++ b/src/EventLogExpert.UI/Filter/SubFilter.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; public sealed record SubFilter(FilterData Data, bool JoinWithAny); diff --git a/src/EventLogExpert.UI/Models/SubFilterDraft.cs b/src/EventLogExpert.UI/Filter/SubFilterDraft.cs similarity index 90% rename from src/EventLogExpert.UI/Models/SubFilterDraft.cs rename to src/EventLogExpert.UI/Filter/SubFilterDraft.cs index a5713b9f..9c1e8b0e 100644 --- a/src/EventLogExpert.UI/Models/SubFilterDraft.cs +++ b/src/EventLogExpert.UI/Filter/SubFilterDraft.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Filter; public sealed class SubFilterDraft { diff --git a/src/EventLogExpert.UI/FilterCache/AddFavoriteFilterAction.cs b/src/EventLogExpert.UI/FilterCache/AddFavoriteFilterAction.cs new file mode 100644 index 00000000..3efe4086 --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/AddFavoriteFilterAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterCache; + +public sealed record AddFavoriteFilterAction(string Filter); diff --git a/src/EventLogExpert.UI/FilterCache/AddFavoriteFilterCompletedAction.cs b/src/EventLogExpert.UI/FilterCache/AddFavoriteFilterCompletedAction.cs new file mode 100644 index 00000000..71d547bb --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/AddFavoriteFilterCompletedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.FilterCache; + +public sealed record AddFavoriteFilterCompletedAction(ImmutableList Filters); diff --git a/src/EventLogExpert.UI/FilterCache/AddRecentFilterAction.cs b/src/EventLogExpert.UI/FilterCache/AddRecentFilterAction.cs new file mode 100644 index 00000000..409376ef --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/AddRecentFilterAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterCache; + +public sealed record AddRecentFilterAction(string Filter); diff --git a/src/EventLogExpert.UI/FilterCache/AddRecentFilterCompletedAction.cs b/src/EventLogExpert.UI/FilterCache/AddRecentFilterCompletedAction.cs new file mode 100644 index 00000000..3db4936d --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/AddRecentFilterCompletedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.FilterCache; + +public sealed record AddRecentFilterCompletedAction(ImmutableQueue Filters); diff --git a/src/EventLogExpert.UI/FilterCache/CacheType.cs b/src/EventLogExpert.UI/FilterCache/CacheType.cs new file mode 100644 index 00000000..1fceaf53 --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/CacheType.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterCache; + +public enum CacheType +{ + Favorites, + Recent +} diff --git a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheEffects.cs b/src/EventLogExpert.UI/FilterCache/Effects.cs similarity index 72% rename from src/EventLogExpert.UI/Store/FilterCache/FilterCacheEffects.cs rename to src/EventLogExpert.UI/FilterCache/Effects.cs index e1fc4c78..5f291bf7 100644 --- a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheEffects.cs +++ b/src/EventLogExpert.UI/FilterCache/Effects.cs @@ -1,18 +1,18 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.FilterCache; +namespace EventLogExpert.UI.FilterCache; -public sealed class FilterCacheEffects(IPreferencesProvider preferencesProvider, IState state) +public sealed class Effects(IPreferencesProvider preferencesProvider, IState state) { private const int MaxRecentFilterCount = 20; [EffectMethod] - public Task HandleAddFavoriteFilter(FilterCacheAction.AddFavoriteFilter action, IDispatcher dispatcher) + public Task HandleAddFavoriteFilter(AddFavoriteFilterAction action, IDispatcher dispatcher) { if (state.Value.FavoriteFilters.Contains(action.Filter)) { return Task.CompletedTask; } @@ -20,13 +20,13 @@ public Task HandleAddFavoriteFilter(FilterCacheAction.AddFavoriteFilter action, preferencesProvider.FavoriteFiltersPreference = newFilters; - dispatcher.Dispatch(new FilterCacheAction.AddFavoriteFilterCompleted(newFilters)); + dispatcher.Dispatch(new AddFavoriteFilterCompletedAction(newFilters)); return Task.CompletedTask; } [EffectMethod] - public Task HandleAddRecentFilter(FilterCacheAction.AddRecentFilter action, IDispatcher dispatcher) + public Task HandleAddRecentFilter(AddRecentFilterAction action, IDispatcher dispatcher) { if (string.IsNullOrWhiteSpace(action.Filter) || state.Value.RecentFilters.Any(filter => @@ -41,13 +41,13 @@ public Task HandleAddRecentFilter(FilterCacheAction.AddRecentFilter action, IDis preferencesProvider.RecentFiltersPreference = newFilters.ToList(); - dispatcher.Dispatch(new FilterCacheAction.AddRecentFilterCompleted(newFilters)); + dispatcher.Dispatch(new AddRecentFilterCompletedAction(newFilters)); return Task.CompletedTask; } [EffectMethod] - public Task HandleImportFavorites(FilterCacheAction.ImportFavorites action, IDispatcher dispatcher) + public Task HandleImportFavorites(ImportFavoritesAction action, IDispatcher dispatcher) { HashSet currentFilters = new(state.Value.FavoriteFilters, StringComparer.OrdinalIgnoreCase); List newFilters = [.. state.Value.FavoriteFilters]; @@ -62,13 +62,13 @@ public Task HandleImportFavorites(FilterCacheAction.ImportFavorites action, IDis preferencesProvider.FavoriteFiltersPreference = newFilters; - dispatcher.Dispatch(new FilterCacheAction.AddFavoriteFilterCompleted([.. newFilters])); + dispatcher.Dispatch(new AddFavoriteFilterCompletedAction([.. newFilters])); return Task.CompletedTask; } [EffectMethod] - public Task HandleLoadFilters(FilterCacheAction.LoadFilters action, IDispatcher dispatcher) + public Task HandleLoadFilters(LoadFiltersAction action, IDispatcher dispatcher) { var favoritesPreference = preferencesProvider.FavoriteFiltersPreference; var recentPreference = preferencesProvider.RecentFiltersPreference; @@ -87,13 +87,13 @@ public Task HandleLoadFilters(FilterCacheAction.LoadFilters action, IDispatcher } dispatcher.Dispatch( - new FilterCacheAction.LoadFiltersCompleted([.. favorites], ImmutableQueue.CreateRange(recent))); + new LoadFiltersCompletedAction([.. favorites], ImmutableQueue.CreateRange(recent))); return Task.CompletedTask; } [EffectMethod] - public Task HandleRemoveFavoriteFilter(FilterCacheAction.RemoveFavoriteFilter action, IDispatcher dispatcher) + public Task HandleRemoveFavoriteFilter(RemoveFavoriteFilterAction action, IDispatcher dispatcher) { if (!state.Value.FavoriteFilters.Contains(action.Filter)) { return Task.CompletedTask; } @@ -120,7 +120,7 @@ public Task HandleRemoveFavoriteFilter(FilterCacheAction.RemoveFavoriteFilter ac preferencesProvider.FavoriteFiltersPreference = favorites; preferencesProvider.RecentFiltersPreference = recent.ToList(); - dispatcher.Dispatch(new FilterCacheAction.RemoveFavoriteFilterCompleted(favorites, recent)); + dispatcher.Dispatch(new RemoveFavoriteFilterCompletedAction(favorites, recent)); return Task.CompletedTask; } diff --git a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheState.cs b/src/EventLogExpert.UI/FilterCache/FilterCacheState.cs similarity index 75% rename from src/EventLogExpert.UI/Store/FilterCache/FilterCacheState.cs rename to src/EventLogExpert.UI/FilterCache/FilterCacheState.cs index 826be570..b2c2e09d 100644 --- a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheState.cs +++ b/src/EventLogExpert.UI/FilterCache/FilterCacheState.cs @@ -1,10 +1,10 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.FilterCache; +namespace EventLogExpert.UI.FilterCache; [FeatureState] public sealed record FilterCacheState diff --git a/src/EventLogExpert.UI/FilterCache/ImportFavoritesAction.cs b/src/EventLogExpert.UI/FilterCache/ImportFavoritesAction.cs new file mode 100644 index 00000000..a88db541 --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/ImportFavoritesAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterCache; + +public sealed record ImportFavoritesAction(List Filters); diff --git a/src/EventLogExpert.UI/FilterCache/LoadFiltersAction.cs b/src/EventLogExpert.UI/FilterCache/LoadFiltersAction.cs new file mode 100644 index 00000000..93a2d565 --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/LoadFiltersAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterCache; + +public sealed record LoadFiltersAction; diff --git a/src/EventLogExpert.UI/FilterCache/LoadFiltersCompletedAction.cs b/src/EventLogExpert.UI/FilterCache/LoadFiltersCompletedAction.cs new file mode 100644 index 00000000..0c460e6f --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/LoadFiltersCompletedAction.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.FilterCache; + +public sealed record LoadFiltersCompletedAction( + ImmutableList FavoriteFilters, + ImmutableQueue RecentFilters); diff --git a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheReducers.cs b/src/EventLogExpert.UI/FilterCache/Reducers.cs similarity index 60% rename from src/EventLogExpert.UI/Store/FilterCache/FilterCacheReducers.cs rename to src/EventLogExpert.UI/FilterCache/Reducers.cs index 9a5d6638..3ba4010c 100644 --- a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheReducers.cs +++ b/src/EventLogExpert.UI/FilterCache/Reducers.cs @@ -1,23 +1,23 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using Fluxor; -namespace EventLogExpert.UI.Store.FilterCache; +namespace EventLogExpert.UI.FilterCache; -public class FilterCacheReducers +internal sealed class Reducers { [ReducerMethod] public static FilterCacheState ReduceAddFavoriteFilterCompleted(FilterCacheState state, - FilterCacheAction.AddFavoriteFilterCompleted action) => state with { FavoriteFilters = action.Filters }; + AddFavoriteFilterCompletedAction action) => state with { FavoriteFilters = action.Filters }; [ReducerMethod] public static FilterCacheState ReduceAddRecentFilterCompleted(FilterCacheState state, - FilterCacheAction.AddRecentFilterCompleted action) => state with { RecentFilters = action.Filters }; + AddRecentFilterCompletedAction action) => state with { RecentFilters = action.Filters }; [ReducerMethod] public static FilterCacheState ReduceLoadFiltersCompleted(FilterCacheState state, - FilterCacheAction.LoadFiltersCompleted action) => state with + LoadFiltersCompletedAction action) => state with { FavoriteFilters = action.FavoriteFilters, RecentFilters = action.RecentFilters @@ -25,7 +25,7 @@ public static FilterCacheState ReduceLoadFiltersCompleted(FilterCacheState state [ReducerMethod] public static FilterCacheState ReduceRemoveFavoriteFilterCompleted(FilterCacheState state, - FilterCacheAction.RemoveFavoriteFilterCompleted action) => state with + RemoveFavoriteFilterCompletedAction action) => state with { FavoriteFilters = action.FavoriteFilters, RecentFilters = action.RecentFilters diff --git a/src/EventLogExpert.UI/FilterCache/RemoveFavoriteFilterAction.cs b/src/EventLogExpert.UI/FilterCache/RemoveFavoriteFilterAction.cs new file mode 100644 index 00000000..750c1aba --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/RemoveFavoriteFilterAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterCache; + +public sealed record RemoveFavoriteFilterAction(string Filter); diff --git a/src/EventLogExpert.UI/FilterCache/RemoveFavoriteFilterCompletedAction.cs b/src/EventLogExpert.UI/FilterCache/RemoveFavoriteFilterCompletedAction.cs new file mode 100644 index 00000000..246a983d --- /dev/null +++ b/src/EventLogExpert.UI/FilterCache/RemoveFavoriteFilterCompletedAction.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.FilterCache; + +public sealed record RemoveFavoriteFilterCompletedAction( + ImmutableList FavoriteFilters, + ImmutableQueue RecentFilters); diff --git a/src/EventLogExpert.UI/FilterGroup/AddGroupAction.cs b/src/EventLogExpert.UI/FilterGroup/AddGroupAction.cs new file mode 100644 index 00000000..104158f3 --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/AddGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record AddGroupAction(SavedFilterGroup? FilterGroup = null); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupEffects.cs b/src/EventLogExpert.UI/FilterGroup/Effects.cs similarity index 66% rename from src/EventLogExpert.UI/Store/FilterGroup/FilterGroupEffects.cs rename to src/EventLogExpert.UI/FilterGroup/Effects.cs index 0564fb4d..8c329c1e 100644 --- a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupEffects.cs +++ b/src/EventLogExpert.UI/FilterGroup/Effects.cs @@ -1,16 +1,16 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using Fluxor; -namespace EventLogExpert.UI.Store.FilterGroup; +namespace EventLogExpert.UI.FilterGroup; -public sealed class FilterGroupEffects( +public sealed class Effects( IState filterGroupState, IPreferencesProvider preferencesProvider) { - [EffectMethod(typeof(FilterGroupAction.AddGroup))] + [EffectMethod(typeof(AddGroupAction))] public Task HandleAddGroup(IDispatcher dispatcher) { preferencesProvider.SavedFiltersPreference = filterGroupState.Value.Groups; @@ -18,7 +18,7 @@ public Task HandleAddGroup(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterGroupAction.ImportGroups))] + [EffectMethod(typeof(ImportGroupsAction))] public Task HandleImportGroups(IDispatcher dispatcher) { preferencesProvider.SavedFiltersPreference = filterGroupState.Value.Groups; @@ -26,17 +26,17 @@ public Task HandleImportGroups(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterGroupAction.LoadGroups))] + [EffectMethod(typeof(LoadGroupsAction))] public Task HandleLoadGroups(IDispatcher dispatcher) { var loadedFilters = preferencesProvider.SavedFiltersPreference; - dispatcher.Dispatch(new FilterGroupAction.LoadGroupsSuccess(loadedFilters)); + dispatcher.Dispatch(new LoadGroupsSuccessAction(loadedFilters)); return Task.CompletedTask; } - [EffectMethod(typeof(FilterGroupAction.RemoveGroup))] + [EffectMethod(typeof(RemoveGroupAction))] public Task HandleRemoveGroup(IDispatcher dispatcher) { preferencesProvider.SavedFiltersPreference = filterGroupState.Value.Groups; @@ -44,7 +44,7 @@ public Task HandleRemoveGroup(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterGroupAction.SetGroup))] + [EffectMethod(typeof(SetGroupAction))] public Task HandleSetGroup(IDispatcher dispatcher) { preferencesProvider.SavedFiltersPreference = filterGroupState.Value.Groups; diff --git a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupState.cs b/src/EventLogExpert.UI/FilterGroup/FilterGroupState.cs similarity index 61% rename from src/EventLogExpert.UI/Store/FilterGroup/FilterGroupState.cs rename to src/EventLogExpert.UI/FilterGroup/FilterGroupState.cs index c10827b3..19b4d8c7 100644 --- a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupState.cs +++ b/src/EventLogExpert.UI/FilterGroup/FilterGroupState.cs @@ -1,16 +1,16 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.FilterGroup; +namespace EventLogExpert.UI.FilterGroup; [FeatureState] public sealed record FilterGroupState { - public ImmutableList Groups { get; init; } = []; + public ImmutableList Groups { get; init; } = []; public IReadOnlyDictionary DisplayGroups { get; init; } = ImmutableDictionary.Empty; diff --git a/src/EventLogExpert.UI/FilterGroup/ImportGroupsAction.cs b/src/EventLogExpert.UI/FilterGroup/ImportGroupsAction.cs new file mode 100644 index 00000000..07a268b6 --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/ImportGroupsAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record ImportGroupsAction(IEnumerable Groups); diff --git a/src/EventLogExpert.UI/FilterGroup/LoadGroupsAction.cs b/src/EventLogExpert.UI/FilterGroup/LoadGroupsAction.cs new file mode 100644 index 00000000..07f22593 --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/LoadGroupsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterGroup; + +internal sealed record LoadGroupsAction; diff --git a/src/EventLogExpert.UI/FilterGroup/LoadGroupsSuccessAction.cs b/src/EventLogExpert.UI/FilterGroup/LoadGroupsSuccessAction.cs new file mode 100644 index 00000000..ead0470f --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/LoadGroupsSuccessAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record LoadGroupsSuccessAction(IEnumerable Groups); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupReducers.cs b/src/EventLogExpert.UI/FilterGroup/Reducers.cs similarity index 84% rename from src/EventLogExpert.UI/Store/FilterGroup/FilterGroupReducers.cs rename to src/EventLogExpert.UI/FilterGroup/Reducers.cs index ab5b4fcb..2af4b9dd 100644 --- a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupReducers.cs +++ b/src/EventLogExpert.UI/FilterGroup/Reducers.cs @@ -1,30 +1,30 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.FilterGroup; +namespace EventLogExpert.UI.FilterGroup; -public sealed class FilterGroupReducers +public sealed class Reducers { [ReducerMethod] - public static FilterGroupState ReducerAddGroup(FilterGroupState state, FilterGroupAction.AddGroup action) => - WithGroups(state, state.Groups.Add(action.FilterGroup ?? new FilterGroupModel())); + public static FilterGroupState ReducerAddGroup(FilterGroupState state, AddGroupAction action) => + WithGroups(state, state.Groups.Add(action.FilterGroup ?? new SavedFilterGroup())); [ReducerMethod] - public static FilterGroupState ReducerImportGroups(FilterGroupState state, FilterGroupAction.ImportGroups action) => + public static FilterGroupState ReducerImportGroups(FilterGroupState state, ImportGroupsAction action) => WithGroups(state, state.Groups.AddRange(action.Groups)); [ReducerMethod] public static FilterGroupState ReducerLoadGroupsSuccess( FilterGroupState state, - FilterGroupAction.LoadGroupsSuccess action) => + LoadGroupsSuccessAction action) => WithGroups(state, [.. action.Groups]); [ReducerMethod] - public static FilterGroupState ReducerRemoveFilter(FilterGroupState state, FilterGroupAction.RemoveFilter action) + public static FilterGroupState ReducerRemoveFilter(FilterGroupState state, RemoveFilterAction action) { var parent = state.Groups.FirstOrDefault(x => x.Id == action.ParentId); @@ -44,7 +44,7 @@ public static FilterGroupState ReducerRemoveFilter(FilterGroupState state, Filte } [ReducerMethod] - public static FilterGroupState ReducerRemoveGroup(FilterGroupState state, FilterGroupAction.RemoveGroup action) + public static FilterGroupState ReducerRemoveGroup(FilterGroupState state, RemoveGroupAction action) { var group = state.Groups.FirstOrDefault(x => x.Id == action.Id); @@ -54,7 +54,7 @@ public static FilterGroupState ReducerRemoveGroup(FilterGroupState state, Filter } [ReducerMethod] - public static FilterGroupState ReducerSetFilter(FilterGroupState state, FilterGroupAction.SetFilter action) + public static FilterGroupState ReducerSetFilter(FilterGroupState state, SetFilterAction action) { var parent = state.Groups.FirstOrDefault(x => x.Id == action.ParentId); @@ -92,7 +92,7 @@ public static FilterGroupState ReducerSetFilter(FilterGroupState state, FilterGr } [ReducerMethod] - public static FilterGroupState ReducerSetGroup(FilterGroupState state, FilterGroupAction.SetGroup action) + public static FilterGroupState ReducerSetGroup(FilterGroupState state, SetGroupAction action) { var group = state.Groups.FirstOrDefault(x => x.Id == action.FilterGroup.Id); @@ -115,14 +115,14 @@ group with [ReducerMethod] public static FilterGroupState ReducerToggleFilterExcluded( FilterGroupState state, - FilterGroupAction.ToggleFilterExcluded action) => + ToggleFilterExcludedAction action) => UpdateFilterInGroup(state, action.ParentId, action.Id, filter => filter with { IsExcluded = !filter.IsExcluded }); [ReducerMethod] - public static FilterGroupState ReducerToggleGroup(FilterGroupState state, FilterGroupAction.ToggleGroup action) + public static FilterGroupState ReducerToggleGroup(FilterGroupState state, ToggleGroupAction action) { var group = state.Groups.FirstOrDefault(x => x.Id == action.Id); @@ -136,7 +136,7 @@ public static FilterGroupState ReducerToggleGroup(FilterGroupState state, Filter } private static IReadOnlyDictionary BuildDisplayGroups( - IEnumerable groups) + IEnumerable groups) { Dictionary displayGroups = []; @@ -150,12 +150,12 @@ private static IReadOnlyDictionary BuildDisplayGroups( return displayGroups.AsReadOnly(); } - private static IReadOnlyList ReplaceFilterById( - IReadOnlyList filters, + private static IReadOnlyList ReplaceFilterById( + IReadOnlyList filters, FilterId id, - FilterModel replacement) + SavedFilter replacement) { - var result = new FilterModel[filters.Count]; + var result = new SavedFilter[filters.Count]; for (var index = 0; index < filters.Count; index++) { @@ -169,7 +169,7 @@ private static FilterGroupState UpdateFilterInGroup( FilterGroupState state, FilterGroupId parentId, FilterId filterId, - Func transform) + Func transform) { var parent = state.Groups.FirstOrDefault(x => x.Id == parentId); @@ -188,6 +188,6 @@ private static FilterGroupState UpdateFilterInGroup( parent with { Filters = ReplaceFilterById(parent.Filters, filterId, transform(filter)) })); } - private static FilterGroupState WithGroups(FilterGroupState state, ImmutableList newGroups) => + private static FilterGroupState WithGroups(FilterGroupState state, ImmutableList newGroups) => state with { Groups = newGroups, DisplayGroups = BuildDisplayGroups(newGroups) }; } diff --git a/src/EventLogExpert.UI/FilterGroup/RemoveFilterAction.cs b/src/EventLogExpert.UI/FilterGroup/RemoveFilterAction.cs new file mode 100644 index 00000000..31f579e8 --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/RemoveFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record RemoveFilterAction(FilterGroupId ParentId, FilterId Id); diff --git a/src/EventLogExpert.UI/FilterGroup/RemoveGroupAction.cs b/src/EventLogExpert.UI/FilterGroup/RemoveGroupAction.cs new file mode 100644 index 00000000..b05f8d1f --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/RemoveGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record RemoveGroupAction(FilterGroupId Id); diff --git a/src/EventLogExpert.UI/FilterGroup/SetFilterAction.cs b/src/EventLogExpert.UI/FilterGroup/SetFilterAction.cs new file mode 100644 index 00000000..3d24f62b --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/SetFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record SetFilterAction(FilterGroupId ParentId, SavedFilter Filter); diff --git a/src/EventLogExpert.UI/FilterGroup/SetGroupAction.cs b/src/EventLogExpert.UI/FilterGroup/SetGroupAction.cs new file mode 100644 index 00000000..b6a7eb2f --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/SetGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record SetGroupAction(SavedFilterGroup FilterGroup); diff --git a/src/EventLogExpert.UI/FilterGroup/ToggleFilterExcludedAction.cs b/src/EventLogExpert.UI/FilterGroup/ToggleFilterExcludedAction.cs new file mode 100644 index 00000000..356e720b --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/ToggleFilterExcludedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record ToggleFilterExcludedAction(FilterGroupId ParentId, FilterId Id); diff --git a/src/EventLogExpert.UI/FilterGroup/ToggleGroupAction.cs b/src/EventLogExpert.UI/FilterGroup/ToggleGroupAction.cs new file mode 100644 index 00000000..25544c13 --- /dev/null +++ b/src/EventLogExpert.UI/FilterGroup/ToggleGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterGroup; + +public sealed record ToggleGroupAction(FilterGroupId Id); diff --git a/src/EventLogExpert.UI/FilterPane/AddFilterAction.cs b/src/EventLogExpert.UI/FilterPane/AddFilterAction.cs new file mode 100644 index 00000000..47499be1 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/AddFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record AddFilterAction(SavedFilter SavedFilter); diff --git a/src/EventLogExpert.UI/FilterPane/ApplyFilterGroupAction.cs b/src/EventLogExpert.UI/FilterPane/ApplyFilterGroupAction.cs new file mode 100644 index 00000000..15bb4fd7 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/ApplyFilterGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record ApplyFilterGroupAction(SavedFilterGroup FilterGroup); diff --git a/src/EventLogExpert.UI/FilterPane/ClearAllFiltersAction.cs b/src/EventLogExpert.UI/FilterPane/ClearAllFiltersAction.cs new file mode 100644 index 00000000..5bbc9976 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/ClearAllFiltersAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterPane; + +internal sealed record ClearAllFiltersAction; diff --git a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneEffects.cs b/src/EventLogExpert.UI/FilterPane/Effects.cs similarity index 63% rename from src/EventLogExpert.UI/Store/FilterPane/FilterPaneEffects.cs rename to src/EventLogExpert.UI/FilterPane/Effects.cs index 719b37eb..2f51034f 100644 --- a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneEffects.cs +++ b/src/EventLogExpert.UI/FilterPane/Effects.cs @@ -1,16 +1,16 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.EventLog; -using EventLogExpert.UI.Store.FilterCache; -using EventLogExpert.UI.Store.FilterGroup; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterCache; +using EventLogExpert.UI.FilterGroup; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.FilterPane; +namespace EventLogExpert.UI.FilterPane; -public sealed class FilterPaneEffects( +public sealed class Effects( IState eventLogState, IState filterPaneState) { @@ -18,38 +18,38 @@ public sealed class FilterPaneEffects( private readonly IState _filterPaneState = filterPaneState; [EffectMethod] - public Task HandleAddFilter(FilterPaneAction.AddFilter action, IDispatcher dispatcher) + public Task HandleAddFilter(AddFilterAction action, IDispatcher dispatcher) { - if (!string.IsNullOrEmpty(action.FilterModel.ComparisonText)) + if (!string.IsNullOrEmpty(action.SavedFilter.ComparisonText)) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); } - if (action.FilterModel.FilterType is not FilterType.Cached && - !string.IsNullOrEmpty(action.FilterModel.ComparisonText)) + if (action.SavedFilter.FilterType is not FilterType.Cached && + !string.IsNullOrEmpty(action.SavedFilter.ComparisonText)) { dispatcher.Dispatch( - new FilterCacheAction.AddRecentFilter(action.FilterModel.ComparisonText)); + new AddRecentFilterAction(action.SavedFilter.ComparisonText)); } return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.ApplyFilterGroup))] + [EffectMethod(typeof(ApplyFilterGroupAction))] public Task HandleApplyFilterGroup(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.ClearAllFilters))] + [EffectMethod(typeof(ClearAllFiltersAction))] public Task HandleClearAllFilters(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.RemoveFilter))] + [EffectMethod(typeof(RemoveFilterAction))] public Task HandleRemoveAdvancedFilter(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); @@ -57,13 +57,13 @@ public Task HandleRemoveAdvancedFilter(IDispatcher dispatcher) } [EffectMethod] - public Task HandleSaveFilterGroup(FilterPaneAction.SaveFilterGroup action, IDispatcher dispatcher) + public Task HandleSaveFilterGroup(SaveFilterGroupAction action, IDispatcher dispatcher) { // New Id so re-applying the group inserts cleanly into the pane's de-dup; IsEnabled cleared // so the user opts in by toggling. All other identity is preserved verbatim via record copy. dispatcher.Dispatch( - new FilterGroupAction.AddGroup( - new FilterGroupModel + new AddGroupAction( + new SavedFilterGroup { Name = action.Name, Filters = @@ -77,31 +77,31 @@ .. _filterPaneState.Value.Filters.Select(filter => } [EffectMethod] - public Task HandleSetFilter(FilterPaneAction.SetFilter action, IDispatcher dispatcher) + public Task HandleSetFilter(SetFilterAction action, IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); - if (!string.IsNullOrEmpty(action.FilterModel.ComparisonText) && - action.FilterModel.FilterType is not FilterType.Cached) + if (!string.IsNullOrEmpty(action.SavedFilter.ComparisonText) && + action.SavedFilter.FilterType is not FilterType.Cached) { - dispatcher.Dispatch(new FilterCacheAction.AddRecentFilter(action.FilterModel.ComparisonText)); + dispatcher.Dispatch(new AddRecentFilterAction(action.SavedFilter.ComparisonText)); } return Task.CompletedTask; } [EffectMethod] - public Task HandleSetFilterDateRange(FilterPaneAction.SetFilterDateRange action, IDispatcher dispatcher) + public Task HandleSetFilterDateRange(SetFilterDateRangeAction action, IDispatcher dispatcher) { - if (action.FilterDateModel is null) + if (action.DateFilter is null) { - dispatcher.Dispatch(new FilterPaneAction.SetFilterDateRangeSuccess(action.FilterDateModel)); + dispatcher.Dispatch(new SetFilterDateRangeSuccessAction(action.DateFilter)); return Task.CompletedTask; } - DateTime? updatedAfter = action.FilterDateModel?.After ?? _filterPaneState.Value.FilteredDateRange?.After; - DateTime? updatedBefore = action.FilterDateModel?.Before ?? _filterPaneState.Value.FilteredDateRange?.Before; + DateTime? updatedAfter = action.DateFilter?.After ?? _filterPaneState.Value.FilteredDateRange?.After; + DateTime? updatedBefore = action.DateFilter?.Before ?? _filterPaneState.Value.FilteredDateRange?.Before; if (updatedAfter is null || updatedBefore is null) { @@ -114,8 +114,8 @@ public Task HandleSetFilterDateRange(FilterPaneAction.SetFilterDateRange action, } dispatcher.Dispatch( - new FilterPaneAction.SetFilterDateRangeSuccess( - new FilterDateModel + new SetFilterDateRangeSuccessAction( + new DateFilter { After = updatedAfter, Before = updatedBefore @@ -124,7 +124,7 @@ public Task HandleSetFilterDateRange(FilterPaneAction.SetFilterDateRange action, return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.SetFilterDateRangeSuccess))] + [EffectMethod(typeof(SetFilterDateRangeSuccessAction))] public Task HandleSetFilterDateRangeSuccess(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); @@ -132,7 +132,7 @@ public Task HandleSetFilterDateRangeSuccess(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.ToggleFilterDate))] + [EffectMethod(typeof(ToggleFilterDateAction))] public Task HandleToggleFilterDate(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); @@ -140,7 +140,7 @@ public Task HandleToggleFilterDate(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.ToggleFilterEnabled))] + [EffectMethod(typeof(ToggleFilterEnabledAction))] public Task HandleToggleFilterEnabled(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); @@ -148,7 +148,7 @@ public Task HandleToggleFilterEnabled(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.ToggleFilterExcluded))] + [EffectMethod(typeof(ToggleFilterExcludedAction))] public Task HandleToggleFilterExcluded(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); @@ -156,7 +156,7 @@ public Task HandleToggleFilterExcluded(IDispatcher dispatcher) return Task.CompletedTask; } - [EffectMethod(typeof(FilterPaneAction.ToggleIsEnabled))] + [EffectMethod(typeof(ToggleIsEnabledAction))] public Task HandleToggleIsEnabled(IDispatcher dispatcher) { UpdateEventTableFilters(_filterPaneState.Value, dispatcher); @@ -180,6 +180,6 @@ private void UpdateEventTableFilters(FilterPaneState filterPaneState, IDispatche return; } - dispatcher.Dispatch(new EventLogAction.SetFilters(candidate)); + dispatcher.Dispatch(new SetFiltersAction(candidate)); } } diff --git a/src/EventLogExpert.UI/FilterPane/FilterPaneState.cs b/src/EventLogExpert.UI/FilterPane/FilterPaneState.cs new file mode 100644 index 00000000..bccb8892 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/FilterPaneState.cs @@ -0,0 +1,20 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; +using Fluxor; +using System.Collections.Immutable; + +namespace EventLogExpert.UI.FilterPane; + +[FeatureState] +public sealed record FilterPaneState +{ + public ImmutableList Filters { get; init; } = []; + + public DateFilter? FilteredDateRange { get; init; } + + public bool IsEnabled { get; init; } = true; + + public bool IsLoading { get; init; } +} diff --git a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneReducers.cs b/src/EventLogExpert.UI/FilterPane/Reducers.cs similarity index 77% rename from src/EventLogExpert.UI/Store/FilterPane/FilterPaneReducers.cs rename to src/EventLogExpert.UI/FilterPane/Reducers.cs index 613a6e73..17e66af1 100644 --- a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneReducers.cs +++ b/src/EventLogExpert.UI/FilterPane/Reducers.cs @@ -1,21 +1,21 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Filter; using Fluxor; -namespace EventLogExpert.UI.Store.FilterPane; +namespace EventLogExpert.UI.FilterPane; -public sealed class FilterPaneReducers +public sealed class Reducers { [ReducerMethod] - public static FilterPaneState ReduceAddFilter(FilterPaneState state, FilterPaneAction.AddFilter action) => - state with { Filters = state.Filters.Add(action.FilterModel) }; + public static FilterPaneState ReduceAddFilter(FilterPaneState state, AddFilterAction action) => + state with { Filters = state.Filters.Add(action.SavedFilter) }; [ReducerMethod] public static FilterPaneState ReduceApplyFilterGroup( FilterPaneState state, - FilterPaneAction.ApplyFilterGroup action) + ApplyFilterGroupAction action) { if (!action.FilterGroup.Filters.Any()) { return state; } @@ -23,7 +23,7 @@ public static FilterPaneState ReduceApplyFilterGroup( HashSet<(string Value, bool IsExcluded)> existingKeys = [.. state.Filters.Select(filter => (filter.ComparisonText, filter.IsExcluded))]; - List additions = []; + List additions = []; foreach (var filter in action.FilterGroup.Filters) { @@ -38,11 +38,11 @@ public static FilterPaneState ReduceApplyFilterGroup( return additions.Count == 0 ? state : state with { Filters = state.Filters.AddRange(additions) }; } - [ReducerMethod(typeof(FilterPaneAction.ClearAllFilters))] + [ReducerMethod(typeof(ClearAllFiltersAction))] public static FilterPaneState ReduceClearFilters(FilterPaneState state) => new() { IsEnabled = state.IsEnabled }; [ReducerMethod] - public static FilterPaneState ReduceRemoveFilter(FilterPaneState state, FilterPaneAction.RemoveFilter action) + public static FilterPaneState ReduceRemoveFilter(FilterPaneState state, RemoveFilterAction action) { var filter = state.Filters.FirstOrDefault(filter => filter.Id == action.Id); @@ -52,28 +52,32 @@ public static FilterPaneState ReduceRemoveFilter(FilterPaneState state, FilterPa } [ReducerMethod] - public static FilterPaneState ReduceSetFilter(FilterPaneState state, FilterPaneAction.SetFilter action) + public static FilterPaneState ReduceSetFilter(FilterPaneState state, SetFilterAction action) { // Upsert: replace-by-Id (preserving position) or append. - var existing = state.Filters.FirstOrDefault(filter => filter.Id == action.FilterModel.Id); + var existing = state.Filters.FirstOrDefault(filter => filter.Id == action.SavedFilter.Id); if (existing is null) { - return state with { Filters = state.Filters.Add(action.FilterModel) }; + return state with { Filters = state.Filters.Add(action.SavedFilter) }; } var index = state.Filters.IndexOf(existing); - return state with { Filters = state.Filters.SetItem(index, action.FilterModel) }; + return state with { Filters = state.Filters.SetItem(index, action.SavedFilter) }; } [ReducerMethod] public static FilterPaneState ReduceSetFilterDateRangeSuccess( FilterPaneState state, - FilterPaneAction.SetFilterDateRangeSuccess action) => - state with { FilteredDateRange = action.FilterDateModel }; + SetFilterDateRangeSuccessAction action) => + state with { FilteredDateRange = action.DateFilter }; - [ReducerMethod(typeof(FilterPaneAction.ToggleFilterDate))] + [ReducerMethod] + public static FilterPaneState ReduceSetIsLoading(FilterPaneState state, SetIsLoadingAction action) => + state.IsLoading == action.IsLoading ? state : state with { IsLoading = action.IsLoading }; + + [ReducerMethod(typeof(ToggleFilterDateAction))] public static FilterPaneState ReduceToggleFilterDate(FilterPaneState state) { if (state.FilteredDateRange is null) { return state; } @@ -87,27 +91,23 @@ public static FilterPaneState ReduceToggleFilterDate(FilterPaneState state) [ReducerMethod] public static FilterPaneState ReduceToggleFilterEnabled( FilterPaneState state, - FilterPaneAction.ToggleFilterEnabled action) => + ToggleFilterEnabledAction action) => UpdateFilterById(state, action.Id, filter => filter with { IsEnabled = !filter.IsEnabled }); [ReducerMethod] public static FilterPaneState ReduceToggleFilterExcluded( FilterPaneState state, - FilterPaneAction.ToggleFilterExcluded action) => + ToggleFilterExcludedAction action) => UpdateFilterById(state, action.Id, filter => filter with { IsExcluded = !filter.IsExcluded }); - [ReducerMethod(typeof(FilterPaneAction.ToggleIsEnabled))] + [ReducerMethod(typeof(ToggleIsEnabledAction))] public static FilterPaneState ReduceToggleIsEnabled(FilterPaneState state) => state with { IsEnabled = !state.IsEnabled }; - [ReducerMethod] - public static FilterPaneState ReduceSetIsLoading(FilterPaneState state, FilterPaneAction.SetIsLoading action) => - state.IsLoading == action.IsLoading ? state : state with { IsLoading = action.IsLoading }; - private static FilterPaneState UpdateFilterById( FilterPaneState state, FilterId id, - Func transform) + Func transform) { var existing = state.Filters.FirstOrDefault(filter => filter.Id == id); diff --git a/src/EventLogExpert.UI/FilterPane/RemoveFilterAction.cs b/src/EventLogExpert.UI/FilterPane/RemoveFilterAction.cs new file mode 100644 index 00000000..4136f738 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/RemoveFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record RemoveFilterAction(FilterId Id); diff --git a/src/EventLogExpert.UI/FilterPane/SaveFilterGroupAction.cs b/src/EventLogExpert.UI/FilterPane/SaveFilterGroupAction.cs new file mode 100644 index 00000000..82d5f081 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/SaveFilterGroupAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterPane; + +public sealed record SaveFilterGroupAction(string Name); diff --git a/src/EventLogExpert.UI/FilterPane/SetFilterAction.cs b/src/EventLogExpert.UI/FilterPane/SetFilterAction.cs new file mode 100644 index 00000000..72d7ea0e --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/SetFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record SetFilterAction(SavedFilter SavedFilter); diff --git a/src/EventLogExpert.UI/FilterPane/SetFilterDateRangeAction.cs b/src/EventLogExpert.UI/FilterPane/SetFilterDateRangeAction.cs new file mode 100644 index 00000000..a7a0ca9f --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/SetFilterDateRangeAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record SetFilterDateRangeAction(DateFilter? DateFilter); diff --git a/src/EventLogExpert.UI/FilterPane/SetFilterDateRangeSuccessAction.cs b/src/EventLogExpert.UI/FilterPane/SetFilterDateRangeSuccessAction.cs new file mode 100644 index 00000000..39301fd8 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/SetFilterDateRangeSuccessAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record SetFilterDateRangeSuccessAction(DateFilter? DateFilter); diff --git a/src/EventLogExpert.UI/FilterPane/SetIsLoadingAction.cs b/src/EventLogExpert.UI/FilterPane/SetIsLoadingAction.cs new file mode 100644 index 00000000..a221e496 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/SetIsLoadingAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterPane; + +public sealed record SetIsLoadingAction(bool IsLoading); diff --git a/src/EventLogExpert.UI/FilterPane/ToggleFilterDateAction.cs b/src/EventLogExpert.UI/FilterPane/ToggleFilterDateAction.cs new file mode 100644 index 00000000..1f65b914 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/ToggleFilterDateAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterPane; + +internal sealed record ToggleFilterDateAction; diff --git a/src/EventLogExpert.UI/FilterPane/ToggleFilterEnabledAction.cs b/src/EventLogExpert.UI/FilterPane/ToggleFilterEnabledAction.cs new file mode 100644 index 00000000..5bf279c3 --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/ToggleFilterEnabledAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record ToggleFilterEnabledAction(FilterId Id); diff --git a/src/EventLogExpert.UI/FilterPane/ToggleFilterExcludedAction.cs b/src/EventLogExpert.UI/FilterPane/ToggleFilterExcludedAction.cs new file mode 100644 index 00000000..ef01e42d --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/ToggleFilterExcludedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Filter; + +namespace EventLogExpert.UI.FilterPane; + +public sealed record ToggleFilterExcludedAction(FilterId Id); diff --git a/src/EventLogExpert.UI/FilterPane/ToggleIsEnabledAction.cs b/src/EventLogExpert.UI/FilterPane/ToggleIsEnabledAction.cs new file mode 100644 index 00000000..565dd15e --- /dev/null +++ b/src/EventLogExpert.UI/FilterPane/ToggleIsEnabledAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.FilterPane; + +internal sealed record ToggleIsEnabledAction; diff --git a/src/EventLogExpert.UI/Interfaces/IClipboardService.cs b/src/EventLogExpert.UI/Interfaces/IClipboardService.cs deleted file mode 100644 index ebd4a2e4..00000000 --- a/src/EventLogExpert.UI/Interfaces/IClipboardService.cs +++ /dev/null @@ -1,20 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.UI.Interfaces; - -public interface IClipboardService -{ - /// - /// Best-effort copy of the current selection (one or more events), or the focused event when nothing is - /// selected, to the clipboard. Implementations must not throw; any failure is logged internally so callers - /// can invoke without try/catch. - /// - Task CopySelectedEvent(CopyType? copyType = null); - - /// - /// Best-effort copy of the supplied text to the clipboard. Implementations must not throw; any failure is - /// logged internally so callers can invoke without try/catch. - /// - Task CopyTextAsync(string text); -} diff --git a/src/EventLogExpert.UI/Interfaces/IFileSaveService.cs b/src/EventLogExpert.UI/Interfaces/IFileSaveService.cs deleted file mode 100644 index 3770cf1f..00000000 --- a/src/EventLogExpert.UI/Interfaces/IFileSaveService.cs +++ /dev/null @@ -1,45 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Collections.Immutable; - -namespace EventLogExpert.UI.Interfaces; - -public interface IFileSaveService -{ - /// - /// Opens a system "Save As" dialog and, only after the user confirms a destination, invokes - /// against a writable stream whose contents are then saved to the destination. - /// Returns the saved path on success, or - /// null if the user cancelled (in which case is never invoked). Throws if the - /// user picked a path but the write or completion failed, or if the host environment cannot present a save dialog - /// (e.g., no MAUI window available); callers should handle failures via try/catch and surface them through - /// . - /// - /// Default filename shown in the dialog (e.g., "debug-log.log"). - /// - /// Group label to extension list (e.g., "Log files" -> [".log", ".txt"]). Must contain at least one entry. - /// - /// - /// Asynchronous writer invoked exactly once with a writable stream after the user confirms a destination. - /// Implementations may stream directly to disk or buffer the entire output in memory before writing to the picked - /// file (the MAUI implementation buffers so that a writer exception leaves the picked file untouched); callers - /// should keep exports reasonably bounded to fit in memory. The stream is owned by the service and disposed once - /// the writer completes; callers must not retain or dispose it. - /// - Task SaveAsync( - string suggestedFileName, - IReadOnlyDictionary> fileTypes, - Func writeContent); -} - -public static class FileSaveServiceFileTypes -{ - public static readonly IReadOnlyDictionary> Json = - ImmutableDictionary>.Empty - .Add("JSON", [".json"]); - - public static readonly IReadOnlyDictionary> Log = - ImmutableDictionary>.Empty - .Add("Log files", [".log", ".txt"]); -} diff --git a/src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs b/src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs deleted file mode 100644 index eff5f545..00000000 --- a/src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs +++ /dev/null @@ -1,29 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.UI.Interfaces; - -/// -/// Describes an alert/prompt hosted inline by an active modal. AcceptLabel null means dismiss-only; -/// IsPrompt true renders an input field below the message. -/// -public sealed record InlineAlertRequest( - string Title, - string Message, - string? AcceptLabel, - string CancelLabel, - bool IsPrompt, - string? PromptInitialValue); - -/// Result of an inline alert. is non-null only for prompt requests. -public sealed record InlineAlertResult(bool Accepted, string? PromptValue); - -/// -/// Implemented by an active modal so alerts can be routed as inline banners instead of opening a separate alert -/// modal (which would cancel the active one). -/// -public interface IInlineAlertHost -{ - /// Show inline. Replaces any prior pending inline alert (its task is canceled). - Task ShowInlineAlertAsync(InlineAlertRequest request, CancellationToken cancellationToken); -} diff --git a/src/EventLogExpert.UI/LogTable/AddTableAction.cs b/src/EventLogExpert.UI/LogTable/AddTableAction.cs new file mode 100644 index 00000000..41eab959 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/AddTableAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record AddTableAction(EventLogData LogData); diff --git a/src/EventLogExpert.UI/LogTable/AppendTableEventsAction.cs b/src/EventLogExpert.UI/LogTable/AppendTableEventsAction.cs new file mode 100644 index 00000000..920abbfc --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/AppendTableEventsAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record AppendTableEventsAction(EventLogId LogId, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/LogTable/AppendTableEventsBatchAction.cs b/src/EventLogExpert.UI/LogTable/AppendTableEventsBatchAction.cs new file mode 100644 index 00000000..ce35dd16 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/AppendTableEventsBatchAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record AppendTableEventsBatchAction(IReadOnlyDictionary> EventsByLog); diff --git a/src/EventLogExpert.UI/LogTable/CloseAllAction.cs b/src/EventLogExpert.UI/LogTable/CloseAllAction.cs new file mode 100644 index 00000000..8d3397ab --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/CloseAllAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/LogTable/CloseLogAction.cs b/src/EventLogExpert.UI/LogTable/CloseLogAction.cs new file mode 100644 index 00000000..80eb159b --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/CloseLogAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record CloseLogAction(EventLogId LogId); diff --git a/src/EventLogExpert.UI/ColumnDefaults.cs b/src/EventLogExpert.UI/LogTable/ColumnDefaults.cs similarity index 95% rename from src/EventLogExpert.UI/ColumnDefaults.cs rename to src/EventLogExpert.UI/LogTable/ColumnDefaults.cs index dee55cb1..8a49bdbe 100644 --- a/src/EventLogExpert.UI/ColumnDefaults.cs +++ b/src/EventLogExpert.UI/LogTable/ColumnDefaults.cs @@ -4,9 +4,9 @@ using System.Collections.Frozen; using System.Collections.Immutable; -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.LogTable; -public static class ColumnDefaults +internal static class ColumnDefaults { public static readonly ImmutableList EnabledColumns = [ diff --git a/src/EventLogExpert.UI/LogTable/ColumnName.cs b/src/EventLogExpert.UI/LogTable/ColumnName.cs new file mode 100644 index 00000000..0bcbb574 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/ColumnName.cs @@ -0,0 +1,22 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace EventLogExpert.UI.LogTable; + +public enum ColumnName +{ + Level, + [EnumMember(Value = "Date and Time")] DateAndTime, + [EnumMember(Value = "Activity ID")] ActivityId, + Log, + [EnumMember(Value = "Computer Name")] ComputerName, + Source, + [EnumMember(Value = "Event ID")] EventId, + [EnumMember(Value = "Task Category")] TaskCategory, + Keywords, + [EnumMember(Value = "Process ID")] ProcessId, + [EnumMember(Value = "Thread ID")] ThreadId, + User +} diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableEffects.cs b/src/EventLogExpert.UI/LogTable/Effects.cs similarity index 74% rename from src/EventLogExpert.UI/Store/EventTable/EventTableEffects.cs rename to src/EventLogExpert.UI/LogTable/Effects.cs index 8cad903f..8d23fe44 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableEffects.cs +++ b/src/EventLogExpert.UI/LogTable/Effects.cs @@ -1,18 +1,18 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.EventTable; +namespace EventLogExpert.UI.LogTable; -public sealed class EventTableEffects(IPreferencesProvider preferencesProvider, IState eventTableState) +public sealed class Effects(IPreferencesProvider preferencesProvider, IState logTableState) { - private readonly IState _eventTableState = eventTableState; + private readonly IState _logTableState = logTableState; private readonly IPreferencesProvider _preferencesProvider = preferencesProvider; - [EffectMethod(typeof(EventTableAction.LoadColumns))] + [EffectMethod(typeof(LoadColumnsAction))] public Task HandleLoadColumns(IDispatcher dispatcher) { var columns = new Dictionary(); @@ -26,21 +26,21 @@ public Task HandleLoadColumns(IDispatcher dispatcher) var widths = BuildWidths(); var order = BuildOrder(); - dispatcher.Dispatch(new EventTableAction.LoadColumnsCompleted(columns, widths, order)); + dispatcher.Dispatch(new LoadColumnsCompletedAction(columns, widths, order)); return Task.CompletedTask; } [EffectMethod] - public Task HandleReorderColumn(EventTableAction.ReorderColumn action, IDispatcher dispatcher) + public Task HandleReorderColumn(ReorderColumnAction action, IDispatcher dispatcher) { // Read from post-reducer state to avoid race conditions with rapid reorder actions - _preferencesProvider.ColumnOrderPreference = _eventTableState.Value.ColumnOrder; + _preferencesProvider.ColumnOrderPreference = _logTableState.Value.ColumnOrder; return Task.CompletedTask; } - [EffectMethod(typeof(EventTableAction.ResetColumnDefaults))] + [EffectMethod(typeof(ResetColumnDefaultsAction))] public Task HandleResetColumnDefaults(IDispatcher dispatcher) { var columns = new Dictionary(); @@ -56,22 +56,22 @@ public Task HandleResetColumnDefaults(IDispatcher dispatcher) var widths = new Dictionary(ColumnDefaults.Widths); - dispatcher.Dispatch(new EventTableAction.LoadColumnsCompleted(columns, widths, ColumnDefaults.Order)); + dispatcher.Dispatch(new LoadColumnsCompletedAction(columns, widths, ColumnDefaults.Order)); return Task.CompletedTask; } [EffectMethod] - public Task HandleSetColumnWidth(EventTableAction.SetColumnWidth action, IDispatcher dispatcher) + public Task HandleSetColumnWidth(SetColumnWidthAction action, IDispatcher dispatcher) { // Read from post-reducer state to avoid race conditions - _preferencesProvider.ColumnWidthsPreference = new Dictionary(_eventTableState.Value.ColumnWidths); + _preferencesProvider.ColumnWidthsPreference = new Dictionary(_logTableState.Value.ColumnWidths); return Task.CompletedTask; } [EffectMethod] - public Task HandleToggleColumn(EventTableAction.ToggleColumn action, IDispatcher dispatcher) + public Task HandleToggleColumn(ToggleColumnAction action, IDispatcher dispatcher) { var columns = new Dictionary(); var enabledColumns = _preferencesProvider.EnabledEventTableColumnsPreference; @@ -89,7 +89,7 @@ public Task HandleToggleColumn(EventTableAction.ToggleColumn action, IDispatcher var widths = BuildWidths(); var order = BuildOrder(); - dispatcher.Dispatch(new EventTableAction.LoadColumnsCompleted(columns, widths, order)); + dispatcher.Dispatch(new LoadColumnsCompletedAction(columns, widths, order)); return Task.CompletedTask; } diff --git a/src/EventLogExpert.UI/LogTable/LoadColumnsAction.cs b/src/EventLogExpert.UI/LogTable/LoadColumnsAction.cs new file mode 100644 index 00000000..fdd54c87 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/LoadColumnsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +internal sealed record LoadColumnsAction; diff --git a/src/EventLogExpert.UI/LogTable/LoadColumnsCompletedAction.cs b/src/EventLogExpert.UI/LogTable/LoadColumnsCompletedAction.cs new file mode 100644 index 00000000..1511709c --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/LoadColumnsCompletedAction.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.LogTable; + +public sealed record LoadColumnsCompletedAction( + IDictionary LoadedColumns, + IDictionary ColumnWidths, + ImmutableList ColumnOrder); diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs b/src/EventLogExpert.UI/LogTable/LogTableState.cs similarity index 82% rename from src/EventLogExpert.UI/Store/EventTable/EventTableState.cs rename to src/EventLogExpert.UI/LogTable/LogTableState.cs index 0e6408a3..f086f65f 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableState.cs +++ b/src/EventLogExpert.UI/LogTable/LogTableState.cs @@ -2,16 +2,16 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.EventLog; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.EventTable; +namespace EventLogExpert.UI.LogTable; [FeatureState] -public sealed record EventTableState +public sealed record LogTableState { - public ImmutableList EventTables { get; init; } = []; + public ImmutableList EventTables { get; init; } = []; public IReadOnlyList DisplayedEvents { get; init; } = []; diff --git a/src/EventLogExpert.UI/Models/EventTableModel.cs b/src/EventLogExpert.UI/LogTable/LogView.cs similarity index 78% rename from src/EventLogExpert.UI/Models/EventTableModel.cs rename to src/EventLogExpert.UI/LogTable/LogView.cs index b5e605c2..f4908308 100644 --- a/src/EventLogExpert.UI/Models/EventTableModel.cs +++ b/src/EventLogExpert.UI/LogTable/LogView.cs @@ -2,10 +2,11 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Channels; +using EventLogExpert.UI.EventLog; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.LogTable; -public sealed record EventTableModel(EventLogId Id) +public sealed record LogView(EventLogId Id) { public string? FileName { get; init; } diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs b/src/EventLogExpert.UI/LogTable/Reducers.cs similarity index 85% rename from src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs rename to src/EventLogExpert.UI/LogTable/Reducers.cs index 468e8f95..4d83c7d2 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs +++ b/src/EventLogExpert.UI/LogTable/Reducers.cs @@ -3,18 +3,19 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Filter; using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.EventTable; +namespace EventLogExpert.UI.LogTable; -public sealed class EventTableReducers +public sealed class Reducers { [ReducerMethod] - public static EventTableState ReduceAddTable(EventTableState state, EventTableAction.AddTable action) + public static LogTableState ReduceAddTable(LogTableState state, AddTableAction action) { - var newTable = new EventTableModel(action.LogData.Id) + var newTable = new LogView(action.LogData.Id) { FileName = action.LogData.Type == LogPathType.Channel ? null : action.LogData.Name, LogName = action.LogData.Name, @@ -45,7 +46,7 @@ public static EventTableState ReduceAddTable(EventTableState state, EventTableAc }; } - combinedTable = new EventTableModel(EventLogId.Create()) { IsCombined = true }; + combinedTable = new LogView(EventLogId.Create()) { IsCombined = true }; return state with { @@ -58,7 +59,7 @@ public static EventTableState ReduceAddTable(EventTableState state, EventTableAc } [ReducerMethod] - public static EventTableState ReduceAppendTableEvents(EventTableState state, EventTableAction.AppendTableEvents action) + public static LogTableState ReduceAppendTableEvents(LogTableState state, AppendTableEventsAction action) { var table = state.EventTables.FirstOrDefault(t => action.LogId == t.Id); @@ -84,9 +85,9 @@ public static EventTableState ReduceAppendTableEvents(EventTableState state, Eve } [ReducerMethod] - public static EventTableState ReduceAppendTableEventsBatch( - EventTableState state, - EventTableAction.AppendTableEventsBatch action) + public static LogTableState ReduceAppendTableEventsBatch( + LogTableState state, + AppendTableEventsBatchAction action) { if (action.EventsByLog.Count == 0) { return state; } @@ -132,8 +133,8 @@ public static EventTableState ReduceAppendTableEventsBatch( }; } - [ReducerMethod(typeof(EventTableAction.CloseAll))] - public static EventTableState ReduceCloseAll(EventTableState state) => + [ReducerMethod(typeof(CloseAllAction))] + public static LogTableState ReduceCloseAll(LogTableState state) => state with { EventTables = [], @@ -143,7 +144,7 @@ state with }; [ReducerMethod] - public static EventTableState ReduceCloseLog(EventTableState state, EventTableAction.CloseLog action) + public static LogTableState ReduceCloseLog(LogTableState state, CloseLogAction action) { var closingTable = state.EventTables.FirstOrDefault(table => table.Id == action.LogId); @@ -180,7 +181,7 @@ public static EventTableState ReduceCloseLog(EventTableState state, EventTableAc } default: { - var combinedTable = new EventTableModel(EventLogId.Create()) { IsCombined = true }; + var combinedTable = new LogView(EventLogId.Create()) { IsCombined = true }; var filtered = FilterOutOwningLog(state.DisplayedEvents, closingTable.LogName); return state with @@ -196,9 +197,9 @@ public static EventTableState ReduceCloseLog(EventTableState state, EventTableAc } [ReducerMethod] - public static EventTableState ReduceLoadColumnsCompleted( - EventTableState state, - EventTableAction.LoadColumnsCompleted action) => + public static LogTableState ReduceLoadColumnsCompleted( + LogTableState state, + LoadColumnsCompletedAction action) => state with { Columns = action.LoadedColumns.ToImmutableDictionary(), @@ -207,7 +208,7 @@ state with }; [ReducerMethod] - public static EventTableState ReduceReorderColumn(EventTableState state, EventTableAction.ReorderColumn action) + public static LogTableState ReduceReorderColumn(LogTableState state, ReorderColumnAction action) { var order = state.ColumnOrder; @@ -226,7 +227,7 @@ public static EventTableState ReduceReorderColumn(EventTableState state, EventTa } [ReducerMethod] - public static EventTableState ReduceSetActiveTable(EventTableState state, EventTableAction.SetActiveTable action) + public static LogTableState ReduceSetActiveTable(LogTableState state, SetActiveTableAction action) { var activeTable = state.EventTables.FirstOrDefault(table => table.Id == action.LogId); @@ -236,17 +237,17 @@ public static EventTableState ReduceSetActiveTable(EventTableState state, EventT } [ReducerMethod] - public static EventTableState ReduceSetColumnWidth(EventTableState state, EventTableAction.SetColumnWidth action) => + public static LogTableState ReduceSetColumnWidth(LogTableState state, SetColumnWidthAction action) => state with { ColumnWidths = state.ColumnWidths.SetItem(action.ColumnName, action.Width) }; [ReducerMethod] - public static EventTableState ReduceSetOrderBy(EventTableState state, EventTableAction.SetOrderBy action) => + public static LogTableState ReduceSetOrderBy(LogTableState state, SetOrderByAction action) => state.OrderBy.Equals(action.OrderBy) ? SortDisplayEvents(state, null, true) : SortDisplayEvents(state, action.OrderBy, state.IsDescending); [ReducerMethod] - public static EventTableState ReduceToggleLoading(EventTableState state, EventTableAction.ToggleLoading action) + public static LogTableState ReduceToggleLoading(LogTableState state, ToggleLoadingAction action) { var table = state.EventTables.FirstOrDefault(table => table.Id == action.LogId); @@ -260,14 +261,14 @@ public static EventTableState ReduceToggleLoading(EventTableState state, EventTa }; } - [ReducerMethod(typeof(EventTableAction.ToggleSorting))] - public static EventTableState ReduceToggleSorting(EventTableState state) => + [ReducerMethod(typeof(ToggleSortingAction))] + public static LogTableState ReduceToggleSorting(LogTableState state) => SortDisplayEvents(state, state.OrderBy, !state.IsDescending); [ReducerMethod] - public static EventTableState ReduceUpdateDisplayedEvents( - EventTableState state, - EventTableAction.UpdateDisplayedEvents action) + public static LogTableState ReduceUpdateDisplayedEvents( + LogTableState state, + UpdateDisplayedEventsAction action) { // Skip log ids absent from EventTables: log closed while filter ran. var tablesById = state.EventTables @@ -356,7 +357,7 @@ public static EventTableState ReduceUpdateDisplayedEvents( } [ReducerMethod] - public static EventTableState ReduceUpdateTable(EventTableState state, EventTableAction.UpdateTable action) + public static LogTableState ReduceUpdateTable(LogTableState state, UpdateTableAction action) { var table = state.EventTables.FirstOrDefault(t => action.LogId == t.Id); @@ -436,7 +437,7 @@ private static ImmutableDictionary IncrementCount( return counts.SetItem(logId, current + delta); } - private static EventTableModel SetComputerNameIfFirstEvent(EventTableModel table, IReadOnlyList newEvents) + private static LogView SetComputerNameIfFirstEvent(LogView table, IReadOnlyList newEvents) { if (!string.IsNullOrEmpty(table.ComputerName) || newEvents.Count == 0) { return table; } @@ -454,7 +455,7 @@ private static EventTableModel SetComputerNameIfFirstEvent(EventTableModel table return table; } - private static EventTableState SortDisplayEvents(EventTableState state, ColumnName? orderBy, bool isDescending) + private static LogTableState SortDisplayEvents(LogTableState state, ColumnName? orderBy, bool isDescending) { var effectiveOrderBy = GetEffectiveOrderBy(orderBy); diff --git a/src/EventLogExpert.UI/LogTable/ReorderColumnAction.cs b/src/EventLogExpert.UI/LogTable/ReorderColumnAction.cs new file mode 100644 index 00000000..fd13e4d5 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/ReorderColumnAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +public sealed record ReorderColumnAction(ColumnName ColumnName, ColumnName TargetColumn, bool InsertAfter); diff --git a/src/EventLogExpert.UI/LogTable/ResetColumnDefaultsAction.cs b/src/EventLogExpert.UI/LogTable/ResetColumnDefaultsAction.cs new file mode 100644 index 00000000..e2ce89b0 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/ResetColumnDefaultsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +internal sealed record ResetColumnDefaultsAction; diff --git a/src/EventLogExpert.UI/LogTable/SetActiveTableAction.cs b/src/EventLogExpert.UI/LogTable/SetActiveTableAction.cs new file mode 100644 index 00000000..66a14c42 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/SetActiveTableAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record SetActiveTableAction(EventLogId LogId); diff --git a/src/EventLogExpert.UI/LogTable/SetColumnWidthAction.cs b/src/EventLogExpert.UI/LogTable/SetColumnWidthAction.cs new file mode 100644 index 00000000..8dc12f19 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/SetColumnWidthAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +public sealed record SetColumnWidthAction(ColumnName ColumnName, int Width); diff --git a/src/EventLogExpert.UI/LogTable/SetOrderByAction.cs b/src/EventLogExpert.UI/LogTable/SetOrderByAction.cs new file mode 100644 index 00000000..ef20dbac --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/SetOrderByAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +public sealed record SetOrderByAction(ColumnName? OrderBy); diff --git a/src/EventLogExpert.UI/LogTable/ToggleColumnAction.cs b/src/EventLogExpert.UI/LogTable/ToggleColumnAction.cs new file mode 100644 index 00000000..5f6192ba --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/ToggleColumnAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +public sealed record ToggleColumnAction(ColumnName ColumnName); diff --git a/src/EventLogExpert.UI/LogTable/ToggleLoadingAction.cs b/src/EventLogExpert.UI/LogTable/ToggleLoadingAction.cs new file mode 100644 index 00000000..0eb2c271 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/ToggleLoadingAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record ToggleLoadingAction(EventLogId LogId); diff --git a/src/EventLogExpert.UI/LogTable/ToggleSortingAction.cs b/src/EventLogExpert.UI/LogTable/ToggleSortingAction.cs new file mode 100644 index 00000000..0aec0d64 --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/ToggleSortingAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.LogTable; + +internal sealed record ToggleSortingAction; diff --git a/src/EventLogExpert.UI/LogTable/UpdateDisplayedEventsAction.cs b/src/EventLogExpert.UI/LogTable/UpdateDisplayedEventsAction.cs new file mode 100644 index 00000000..824a722a --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/UpdateDisplayedEventsAction.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record UpdateDisplayedEventsAction( + IReadOnlyDictionary> ActiveLogs); diff --git a/src/EventLogExpert.UI/LogTable/UpdateTableAction.cs b/src/EventLogExpert.UI/LogTable/UpdateTableAction.cs new file mode 100644 index 00000000..8577dacf --- /dev/null +++ b/src/EventLogExpert.UI/LogTable/UpdateTableAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.EventLog; + +namespace EventLogExpert.UI.LogTable; + +public sealed record UpdateTableAction(EventLogId LogId, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs b/src/EventLogExpert.UI/Menu/IMenuActionService.cs similarity index 80% rename from src/EventLogExpert.UI/Interfaces/IMenuActionService.cs rename to src/EventLogExpert.UI/Menu/IMenuActionService.cs index e8c32532..7a59b6e7 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs +++ b/src/EventLogExpert.UI/Menu/IMenuActionService.cs @@ -1,7 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Clipboard; + +namespace EventLogExpert.UI.Menu; public interface IMenuActionService { @@ -11,13 +13,13 @@ public interface IMenuActionService Task CloseAllLogsAsync(); - Task CopySelectedAsync(CopyType? copyType); + Task CopySelectedAsync(EventCopyFormat? format); void Exit(); /// - /// Returns the names of every event log channel known to the host. Cached on first call; - /// subsequent calls return immediately. + /// Returns the names of every event log channel known to the host. Cached on first call; subsequent calls return + /// immediately. /// Task> GetOtherLogNamesAsync(); diff --git a/src/EventLogExpert.UI/Interfaces/IMenuService.cs b/src/EventLogExpert.UI/Menu/IMenuService.cs similarity index 96% rename from src/EventLogExpert.UI/Interfaces/IMenuService.cs rename to src/EventLogExpert.UI/Menu/IMenuService.cs index 92d3b905..946670f4 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuService.cs +++ b/src/EventLogExpert.UI/Menu/IMenuService.cs @@ -1,9 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; - -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Menu; /// /// Coordinates a single active popup menu at a time. Hosts subscribe to and render diff --git a/src/EventLogExpert.UI/Models/MenuItem.cs b/src/EventLogExpert.UI/Menu/MenuItem.cs similarity index 84% rename from src/EventLogExpert.UI/Models/MenuItem.cs rename to src/EventLogExpert.UI/Menu/MenuItem.cs index 4b8f47e5..b35c4048 100644 --- a/src/EventLogExpert.UI/Models/MenuItem.cs +++ b/src/EventLogExpert.UI/Menu/MenuItem.cs @@ -1,12 +1,12 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Menu; /// -/// Immutable description of a single entry in a menu (menu bar dropdown, context menu, or submenu). -/// Used by the data-driven MenuRenderer; pair instances via or -/// to build trees. +/// Immutable description of a single entry in a menu (menu bar dropdown, context menu, or submenu). Used by the +/// data-driven MenuRenderer; pair instances via or to build +/// trees. /// public sealed record MenuItem { @@ -23,8 +23,8 @@ public sealed record MenuItem public Func>>? ChildrenLoader { get; init; } /// - /// null for non-checkable items; true/false for checkable items so screen - /// readers can announce the toggle state via role="menuitemcheckbox" + aria-checked. + /// null for non-checkable items; true/false for checkable items so screen readers can + /// announce the toggle state via role="menuitemcheckbox" + aria-checked. /// public bool? IsChecked { get; init; } diff --git a/src/EventLogExpert.UI/Services/MenuService.cs b/src/EventLogExpert.UI/Menu/MenuService.cs similarity index 93% rename from src/EventLogExpert.UI/Services/MenuService.cs rename to src/EventLogExpert.UI/Menu/MenuService.cs index 2ad9fe44..34a2ac6a 100644 --- a/src/EventLogExpert.UI/Services/MenuService.cs +++ b/src/EventLogExpert.UI/Menu/MenuService.cs @@ -1,10 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; - -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Menu; public sealed class MenuService : IMenuService { diff --git a/src/EventLogExpert.UI/Interfaces/IModalService.cs b/src/EventLogExpert.UI/Modal/IModalService.cs similarity index 71% rename from src/EventLogExpert.UI/Interfaces/IModalService.cs rename to src/EventLogExpert.UI/Modal/IModalService.cs index 4e3029be..0983e486 100644 --- a/src/EventLogExpert.UI/Interfaces/IModalService.cs +++ b/src/EventLogExpert.UI/Modal/IModalService.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Components; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Modal; /// /// Coordinates a single active modal at a time. Hosts subscribe to and render @@ -25,15 +25,7 @@ public interface IModalService /// Complete the active modal's task. Stale ids (from replaced modals) are ignored. void Complete(long modalId, TResult? result); - /// Register the active modal as the inline-alert host. Stale ids are ignored. - void RegisterActiveAlertHost(long modalId, IInlineAlertHost host); - /// Open a modal. Any prior active modal is canceled (its task completes with default). Task Show(IDictionary? parameters = null) where TModal : IComponent; - - /// Returns the currently registered alert host, if any. Inspect on every alert. - bool TryGetActiveAlertHost(out IInlineAlertHost? host); - - void UnregisterActiveAlertHost(long modalId); } diff --git a/src/EventLogExpert.UI/Services/ModalService.cs b/src/EventLogExpert.UI/Modal/ModalService.cs similarity index 61% rename from src/EventLogExpert.UI/Services/ModalService.cs rename to src/EventLogExpert.UI/Modal/ModalService.cs index 8e4ce45b..d0ae59d5 100644 --- a/src/EventLogExpert.UI/Services/ModalService.cs +++ b/src/EventLogExpert.UI/Modal/ModalService.cs @@ -1,24 +1,22 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; using Microsoft.AspNetCore.Components; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Modal; public sealed class ModalService : IModalService { private readonly Lock _stateLock = new(); - private IInlineAlertHost? _activeAlertHost; - private long _activeAlertHostId; + private long _activeModalId; private object? _activeTcs; private Action? _cancelActiveDelegate; private long _idCounter; public event Action? StateChanged; - public long ActiveModalId { get; private set; } + public long ActiveModalId => Volatile.Read(ref _activeModalId); public IDictionary? ActiveModalParameters { get; private set; } @@ -46,7 +44,7 @@ public void Complete(long modalId, TResult? result) lock (_stateLock) { - if (modalId != ActiveModalId) { return; } + if (modalId != _activeModalId) { return; } if (_activeTcs is not TaskCompletionSource tcs) { @@ -63,20 +61,6 @@ public void Complete(long modalId, TResult? result) if (stateChanged) { StateChanged?.Invoke(); } } - public void RegisterActiveAlertHost(long modalId, IInlineAlertHost host) - { - ArgumentNullException.ThrowIfNull(host); - - lock (_stateLock) - { - // Late registration from a stale modal would route alerts to a torn-down host. - if (modalId != ActiveModalId) { return; } - - _activeAlertHost = host; - _activeAlertHostId = modalId; - } - } - public Task Show(IDictionary? parameters = null) where TModal : IComponent { @@ -87,51 +71,26 @@ public void RegisterActiveAlertHost(long modalId, IInlineAlertHost host) _cancelActiveDelegate?.Invoke(); _idCounter++; - ActiveModalId = _idCounter; + Volatile.Write(ref _activeModalId, _idCounter); ActiveModalType = typeof(TModal); ActiveModalParameters = parameters; _activeTcs = tcs; _cancelActiveDelegate = () => tcs.TrySetResult(default); - - // New modal will re-register itself in OnInitialized. - _activeAlertHost = null; - _activeAlertHostId = 0; } StateChanged?.Invoke(); return tcs.Task; } - public bool TryGetActiveAlertHost(out IInlineAlertHost? host) - { - lock (_stateLock) - { - host = _activeAlertHost; - return host is not null; - } - } - - public void UnregisterActiveAlertHost(long modalId) - { - lock (_stateLock) - { - if (modalId != _activeAlertHostId) { return; } - - _activeAlertHost = null; - _activeAlertHostId = 0; - } - } - private void ClearStateLocked() { // Sentinel id 0 < any id issued by Show() (idCounter is pre-incremented), so any stale - // modalId fails the equality check in RegisterActiveAlertHost/Complete after this clear. - ActiveModalId = 0; + // modalId fails the equality check in Complete after this clear. The same id change makes + // the inline-alert host broker lazily invalidate any host registered against the prior id. + Volatile.Write(ref _activeModalId, 0); ActiveModalType = null; ActiveModalParameters = null; _activeTcs = null; _cancelActiveDelegate = null; - _activeAlertHost = null; - _activeAlertHostId = 0; } } diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchCompletedEventArgs.cs b/src/EventLogExpert.UI/Models/UpgradeBatchCompletedEventArgs.cs deleted file mode 100644 index 469dab0e..00000000 --- a/src/EventLogExpert.UI/Models/UpgradeBatchCompletedEventArgs.cs +++ /dev/null @@ -1,13 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.UI.Models; - -public sealed class UpgradeBatchCompletedEventArgs(Guid batchId, UpgradeBatchResult result, bool wasCancelled) : EventArgs -{ - public Guid BatchId { get; } = batchId; - - public UpgradeBatchResult Result { get; } = result; - - public bool WasCancelled { get; } = wasCancelled; -} diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchProgressEventArgs.cs b/src/EventLogExpert.UI/Models/UpgradeBatchProgressEventArgs.cs deleted file mode 100644 index b6d8452e..00000000 --- a/src/EventLogExpert.UI/Models/UpgradeBatchProgressEventArgs.cs +++ /dev/null @@ -1,15 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.UI.Models; - -public sealed class UpgradeBatchProgressEventArgs(Guid batchId, int position, string fileName, UpgradePhase phase) : EventArgs -{ - public Guid BatchId { get; } = batchId; - - public string FileName { get; } = fileName; - - public UpgradePhase Phase { get; } = phase; - - public int Position { get; } = position; -} diff --git a/src/EventLogExpert.UI/Services/AlertPresentation.cs b/src/EventLogExpert.UI/Services/AlertPresentation.cs deleted file mode 100644 index 9af97c55..00000000 --- a/src/EventLogExpert.UI/Services/AlertPresentation.cs +++ /dev/null @@ -1,35 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.UI.Interfaces; - -namespace EventLogExpert.UI.Services; - -/// -/// Controls how an request is -/// surfaced to the user. -/// -public enum AlertPresentation -{ - /// - /// Default. Use the existing routing: render inline in the active modal host - /// if one is registered, otherwise open a standalone alert popup. - /// - Auto, - - /// - /// Route to with severity. Only - /// valid for one-button overloads (the banner has no accept/cancel pair); using it on a two-button overload throws. - /// - Banner, - - /// - /// Require an active inline alert host. Throws if none is registered. - /// - InlineOnly, - - /// - /// Always open a standalone popup, even if an inline host is registered. - /// - PopupOnly, -} diff --git a/src/EventLogExpert.UI/Services/MainThreadService.cs b/src/EventLogExpert.UI/Services/MainThreadService.cs deleted file mode 100644 index 4b4c6fe2..00000000 --- a/src/EventLogExpert.UI/Services/MainThreadService.cs +++ /dev/null @@ -1,44 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.UI.Services; - -public interface IMainThreadService -{ - Task InvokeOnMainThread(Action action); - - /// Invoke an asynchronous delegate on the main thread and await it. - Task InvokeOnMainThreadAsync(Func action); -} - -public class MainThreadService( - Func mainThreadInvoker, - Func, Task>? mainThreadAsyncInvoker = null) : IMainThreadService -{ - private readonly Func, Task>? _mainThreadAsyncInvoker = mainThreadAsyncInvoker; - private readonly Func _mainThreadInvoker = mainThreadInvoker; - - public async Task InvokeOnMainThread(Action action) - { - await _mainThreadInvoker(action); - } - - public async Task InvokeOnMainThreadAsync(Func action) - { - if (_mainThreadAsyncInvoker is not null) - { - await _mainThreadAsyncInvoker(action); - return; - } - - // Fallback: marshal via the sync overload, capturing the inner Task so exceptions and - // completion propagate. - Task? inner = null; - await _mainThreadInvoker(() => { inner = action(); }); - - if (inner is not null) - { - await inner; - } - } -} diff --git a/src/EventLogExpert.UI/Interfaces/ISettingsService.cs b/src/EventLogExpert.UI/Settings/ISettingsService.cs similarity index 70% rename from src/EventLogExpert.UI/Interfaces/ISettingsService.cs rename to src/EventLogExpert.UI/Settings/ISettingsService.cs index 7f6c23cb..473fb26a 100644 --- a/src/EventLogExpert.UI/Interfaces/ISettingsService.cs +++ b/src/EventLogExpert.UI/Settings/ISettingsService.cs @@ -1,15 +1,16 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Clipboard; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Settings; public interface ISettingsService { - CopyType CopyType { get; set; } + EventCopyFormat CopyFormat { get; set; } - Action? CopyTypeChanged { get; set; } + Action? CopyFormatChanged { get; set; } bool IsPreReleaseEnabled { get; set; } diff --git a/src/EventLogExpert.UI/Services/SettingsService.cs b/src/EventLogExpert.UI/Settings/SettingsService.cs similarity index 82% rename from src/EventLogExpert.UI/Services/SettingsService.cs rename to src/EventLogExpert.UI/Settings/SettingsService.cs index 95e8f097..91287fb6 100644 --- a/src/EventLogExpert.UI/Services/SettingsService.cs +++ b/src/EventLogExpert.UI/Settings/SettingsService.cs @@ -1,41 +1,42 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Preferences; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Settings; public sealed class SettingsService(IPreferencesProvider preferences) : ISettingsService { private readonly IPreferencesProvider _preferences = preferences; - private CopyType? _copyType; + private EventCopyFormat? _copyFormat; private bool? _isPreReleaseEnabled; private LogLevel? _logLevel; private bool? _showDisplayPaneOnSelectionChange; private Theme? _theme; private string? _timeZoneId; - public CopyType CopyType + public EventCopyFormat CopyFormat { get { - _copyType ??= _preferences.KeyboardCopyTypePreference; + _copyFormat ??= _preferences.KeyboardCopyFormatPreference; - return _copyType ?? CopyType.Default; + return _copyFormat ?? EventCopyFormat.Default; } set { - if (_copyType == value) { return; } + if (_copyFormat == value) { return; } - _copyType = value; - _preferences.KeyboardCopyTypePreference = value; - CopyTypeChanged?.Invoke(); + _copyFormat = value; + _preferences.KeyboardCopyFormatPreference = value; + CopyFormatChanged?.Invoke(); } } - public Action? CopyTypeChanged { get; set; } + public Action? CopyFormatChanged { get; set; } public bool IsPreReleaseEnabled { diff --git a/src/EventLogExpert.UI/Settings/Theme.cs b/src/EventLogExpert.UI/Settings/Theme.cs new file mode 100644 index 00000000..515f6f1a --- /dev/null +++ b/src/EventLogExpert.UI/Settings/Theme.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Settings; + +public enum Theme +{ + System, + Light, + Dark +} diff --git a/src/EventLogExpert.UI/StatusBar/ClearStatusAction.cs b/src/EventLogExpert.UI/StatusBar/ClearStatusAction.cs new file mode 100644 index 00000000..bb5e60e1 --- /dev/null +++ b/src/EventLogExpert.UI/StatusBar/ClearStatusAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.StatusBar; + +public sealed record ClearStatusAction(StatusActivityId ActivityId); diff --git a/src/EventLogExpert.UI/StatusBar/CloseAllAction.cs b/src/EventLogExpert.UI/StatusBar/CloseAllAction.cs new file mode 100644 index 00000000..33333f88 --- /dev/null +++ b/src/EventLogExpert.UI/StatusBar/CloseAllAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.StatusBar; + +public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/Store/StatusBar/StatusBarReducers.cs b/src/EventLogExpert.UI/StatusBar/Reducers.cs similarity index 71% rename from src/EventLogExpert.UI/Store/StatusBar/StatusBarReducers.cs rename to src/EventLogExpert.UI/StatusBar/Reducers.cs index 04e5063e..821a53f8 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/StatusBarReducers.cs +++ b/src/EventLogExpert.UI/StatusBar/Reducers.cs @@ -1,15 +1,15 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; -public sealed class StatusBarReducers +public sealed class Reducers { [ReducerMethod] - public static StatusBarState ReduceClearStatus(StatusBarState state, StatusBarAction.ClearStatus action) + public static StatusBarState ReduceClearStatus(StatusBarState state, ClearStatusAction action) { var updatedState = state with { }; @@ -21,11 +21,11 @@ public static StatusBarState ReduceClearStatus(StatusBarState state, StatusBarAc return updatedState; } - [ReducerMethod(typeof(StatusBarAction.CloseAll))] + [ReducerMethod(typeof(CloseAllAction))] public static StatusBarState ReduceCloseAll(StatusBarState state) => new(); [ReducerMethod] - public static StatusBarState ReduceSetEventsLoading(StatusBarState state, StatusBarAction.SetEventsLoading action) + public static StatusBarState ReduceSetEventsLoading(StatusBarState state, SetEventsLoadingAction action) { var newLoading = CommonLoadingReducer(state.EventsLoading, action.ActivityId, action.Count, action.FailedCount); @@ -34,12 +34,12 @@ public static StatusBarState ReduceSetEventsLoading(StatusBarState state, Status [ReducerMethod] public static StatusBarState - ReduceSetResolverStatus(StatusBarState state, StatusBarAction.SetResolverStatus action) => + ReduceSetResolverStatus(StatusBarState state, SetResolverStatusAction action) => new() { ResolverStatus = action.ResolverStatus }; - private static ImmutableDictionary CommonLoadingReducer( - ImmutableDictionary loadingEntries, - Guid activityId, + private static ImmutableDictionary CommonLoadingReducer( + ImmutableDictionary loadingEntries, + StatusActivityId activityId, int count, int failedCount) { diff --git a/src/EventLogExpert.UI/StatusBar/SetEventsLoadingAction.cs b/src/EventLogExpert.UI/StatusBar/SetEventsLoadingAction.cs new file mode 100644 index 00000000..b21bd165 --- /dev/null +++ b/src/EventLogExpert.UI/StatusBar/SetEventsLoadingAction.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.StatusBar; + +/// Used to indicate the progress of event logs being loaded. +/// +/// A unique id that distinguishes this loading activity from others, since log names such as +/// Application will be common and many file names will be the same. +/// +/// +public sealed record SetEventsLoadingAction(StatusActivityId ActivityId, int Count, int FailedCount); diff --git a/src/EventLogExpert.UI/StatusBar/SetResolverStatusAction.cs b/src/EventLogExpert.UI/StatusBar/SetResolverStatusAction.cs new file mode 100644 index 00000000..639b033f --- /dev/null +++ b/src/EventLogExpert.UI/StatusBar/SetResolverStatusAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.StatusBar; + +public sealed record SetResolverStatusAction(string ResolverStatus); diff --git a/src/EventLogExpert.UI/StatusBar/StatusActivityId.cs b/src/EventLogExpert.UI/StatusBar/StatusActivityId.cs new file mode 100644 index 00000000..1ed87219 --- /dev/null +++ b/src/EventLogExpert.UI/StatusBar/StatusActivityId.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.StatusBar; + +public readonly record struct StatusActivityId(Guid Value) +{ + public static StatusActivityId Create() => new(Guid.NewGuid()); +} diff --git a/src/EventLogExpert.UI/Store/StatusBar/StatusBarState.cs b/src/EventLogExpert.UI/StatusBar/StatusBarState.cs similarity index 52% rename from src/EventLogExpert.UI/Store/StatusBar/StatusBarState.cs rename to src/EventLogExpert.UI/StatusBar/StatusBarState.cs index 7a10c607..40081e43 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/StatusBarState.cs +++ b/src/EventLogExpert.UI/StatusBar/StatusBarState.cs @@ -1,15 +1,15 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; [FeatureState(MaximumStateChangedNotificationsPerSecond = 1)] public sealed record StatusBarState { - public ImmutableDictionary EventsLoading { get; init; } = ImmutableDictionary.Empty; + public ImmutableDictionary EventsLoading { get; init; } = ImmutableDictionary.Empty; public string ResolverStatus { get; init; } = string.Empty; } diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs b/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs deleted file mode 100644 index 4ff899f3..00000000 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs +++ /dev/null @@ -1,56 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.Eventing.Common.Channels; -using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; -using System.Collections.Immutable; - -namespace EventLogExpert.UI.Store.EventLog; - -public sealed record EventLogAction -{ - public sealed record AddEvent(ResolvedEvent NewEvent); - - public sealed record AddEventBuffered(IReadOnlyList UpdatedBuffer, bool IsFull); - - public sealed record AddEventSuccess(ImmutableDictionary ActiveLogs); - - public sealed record CloseAll; - - public sealed record CloseLog(EventLogId LogId, string LogName); - - public sealed record LoadEvents(EventLogData LogData, IReadOnlyList Events); - - public sealed record LoadEventsPartial(EventLogData LogData, IReadOnlyList Events); - - public sealed record LoadNewEvents; - - public sealed record OpenLog(string LogName, LogPathType LogPathType, CancellationToken Token = default); - - public sealed record SelectEvent( - ResolvedEvent SelectedEvent, - bool IsMultiSelect = false, - bool ShouldStaySelected = false); - - public sealed record SelectEvents(IReadOnlyCollection SelectedEvents); - - /// - /// Replaces the entire selection with the supplied events, preserving input order - /// and de-duplicating by reference identity, and atomically updates the focused - /// (selected) event. Use this for range selection (Shift+Click, Shift+Arrow), - /// Select All (Ctrl+A), clear (Escape), and any selection update where the caller - /// already knows both the new selection and the new focus. - /// - /// The new selection list, in the order that should be - /// preserved (typically the current table's sort order). - /// The new focused event, or null to clear focus. Does not - /// need to be a member of . - public sealed record SetSelectedEvents( - IReadOnlyCollection SelectedEvents, - ResolvedEvent? SelectedEvent); - - public sealed record SetContinuouslyUpdate(bool ContinuouslyUpdate); - - public sealed record SetFilters(EventFilter EventFilter); -} diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs b/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs deleted file mode 100644 index c5ff8fe9..00000000 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs +++ /dev/null @@ -1,49 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.Eventing.Common.Events; -using EventLogExpert.UI.Models; -using System.Collections.Immutable; - -namespace EventLogExpert.UI.Store.EventTable; - -public sealed record EventTableAction -{ - public sealed record AddTable(EventLogData LogData); - - public sealed record AppendTableEvents(EventLogId LogId, IReadOnlyList Events); - - public sealed record AppendTableEventsBatch(IReadOnlyDictionary> EventsByLog); - - public sealed record CloseAll; - - public sealed record CloseLog(EventLogId LogId); - - public sealed record LoadColumns; - - public sealed record LoadColumnsCompleted( - IDictionary LoadedColumns, - IDictionary ColumnWidths, - ImmutableList ColumnOrder); - - public sealed record ReorderColumn(ColumnName ColumnName, ColumnName TargetColumn, bool InsertAfter); - - public sealed record ResetColumnDefaults; - - public sealed record SetActiveTable(EventLogId LogId); - - public sealed record SetColumnWidth(ColumnName ColumnName, int Width); - - public sealed record SetOrderBy(ColumnName? OrderBy); - - public sealed record ToggleColumn(ColumnName ColumnName); - - public sealed record ToggleLoading(EventLogId LogId); - - public sealed record ToggleSorting; - - public sealed record UpdateDisplayedEvents( - IReadOnlyDictionary> ActiveLogs); - - public sealed record UpdateTable(EventLogId LogId, IReadOnlyList Events); -} diff --git a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheAction.cs b/src/EventLogExpert.UI/Store/FilterCache/FilterCacheAction.cs deleted file mode 100644 index 88dc851c..00000000 --- a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheAction.cs +++ /dev/null @@ -1,31 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using System.Collections.Immutable; - -namespace EventLogExpert.UI.Store.FilterCache; - -public sealed record FilterCacheAction -{ - public sealed record AddFavoriteFilter(string Filter); - - public sealed record AddFavoriteFilterCompleted(ImmutableList Filters); - - public sealed record AddRecentFilter(string Filter); - - public sealed record AddRecentFilterCompleted(ImmutableQueue Filters); - - public sealed record ImportFavorites(List Filters); - - public sealed record LoadFilters; - - public sealed record LoadFiltersCompleted( - ImmutableList FavoriteFilters, - ImmutableQueue RecentFilters); - - public sealed record RemoveFavoriteFilter(string Filter); - - public sealed record RemoveFavoriteFilterCompleted( - ImmutableList FavoriteFilters, - ImmutableQueue RecentFilters); -} diff --git a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupAction.cs deleted file mode 100644 index 8bde5a72..00000000 --- a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupAction.cs +++ /dev/null @@ -1,29 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.UI.Models; - -namespace EventLogExpert.UI.Store.FilterGroup; - -public sealed record FilterGroupAction -{ - public sealed record AddGroup(FilterGroupModel? FilterGroup = null); - - public sealed record ImportGroups(IEnumerable Groups); - - public sealed record LoadGroups; - - public sealed record LoadGroupsSuccess(IEnumerable Groups); - - public sealed record RemoveFilter(FilterGroupId ParentId, FilterId Id); - - public sealed record RemoveGroup(FilterGroupId Id); - - public sealed record SetFilter(FilterGroupId ParentId, FilterModel Filter); - - public sealed record SetGroup(FilterGroupModel FilterGroup); - - public sealed record ToggleFilterExcluded(FilterGroupId ParentId, FilterId Id); - - public sealed record ToggleGroup(FilterGroupId Id); -} diff --git a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneAction.cs b/src/EventLogExpert.UI/Store/FilterPane/FilterPaneAction.cs deleted file mode 100644 index fc8fed62..00000000 --- a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneAction.cs +++ /dev/null @@ -1,35 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.UI.Models; - -namespace EventLogExpert.UI.Store.FilterPane; - -public sealed record FilterPaneAction -{ - public sealed record AddFilter(FilterModel FilterModel); - - public sealed record ApplyFilterGroup(FilterGroupModel FilterGroup); - - public sealed record ClearAllFilters; - - public sealed record RemoveFilter(FilterId Id); - - public sealed record SaveFilterGroup(string Name); - - public sealed record SetFilter(FilterModel FilterModel); - - public sealed record SetFilterDateRange(FilterDateModel? FilterDateModel); - - public sealed record SetFilterDateRangeSuccess(FilterDateModel? FilterDateModel); - - public sealed record SetIsLoading(bool IsLoading); - - public sealed record ToggleFilterEnabled(FilterId Id); - - public sealed record ToggleFilterExcluded(FilterId Id); - - public sealed record ToggleFilterDate; - - public sealed record ToggleIsEnabled; -} diff --git a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneState.cs b/src/EventLogExpert.UI/Store/FilterPane/FilterPaneState.cs deleted file mode 100644 index 5a9548f6..00000000 --- a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneState.cs +++ /dev/null @@ -1,20 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -using EventLogExpert.UI.Models; -using Fluxor; -using System.Collections.Immutable; - -namespace EventLogExpert.UI.Store.FilterPane; - -[FeatureState] -public sealed record FilterPaneState -{ - public ImmutableList Filters { get; init; } = []; - - public FilterDateModel? FilteredDateRange { get; init; } - - public bool IsEnabled { get; init; } = true; - - public bool IsLoading { get; init; } -} diff --git a/src/EventLogExpert.UI/Store/StatusBar/StatusBarAction.cs b/src/EventLogExpert.UI/Store/StatusBar/StatusBarAction.cs deleted file mode 100644 index d6818dca..00000000 --- a/src/EventLogExpert.UI/Store/StatusBar/StatusBarAction.cs +++ /dev/null @@ -1,21 +0,0 @@ -// // Copyright (c) Microsoft Corporation. -// // Licensed under the MIT License. - -namespace EventLogExpert.UI.Store.StatusBar; - -public sealed record StatusBarAction -{ - public sealed record ClearStatus(Guid ActivityId); - - public sealed record CloseAll; - - /// Used to indicate the progress of event logs being loaded. - /// - /// A unique id that distinguishes this loading activity from others, since log names such as - /// Application will be common and many file names will be the same. - /// - /// - public sealed record SetEventsLoading(Guid ActivityId, int Count, int FailedCount); - - public sealed record SetResolverStatus(string ResolverStatus); -} diff --git a/src/EventLogExpert.UI/Services/DeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/DeploymentService.cs similarity index 94% rename from src/EventLogExpert.UI/Services/DeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/DeploymentService.cs index 904a76e3..89a195e2 100644 --- a/src/EventLogExpert.UI/Services/DeploymentService.cs +++ b/src/EventLogExpert.UI/Update/Deployment/DeploymentService.cs @@ -2,15 +2,17 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Options; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Restart; +using EventLogExpert.UI.Common.Threading; using System.Reflection; using Windows.Foundation; using Windows.Management.Deployment; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update.Deployment; -public class DeploymentService( +public sealed class DeploymentService( ITraceLogger traceLogger, IAppTitleService appTitleService, IMainThreadService mainThreadService, @@ -19,8 +21,8 @@ public class DeploymentService( IPackageDeploymentService packageDeploymentService) : IDeploymentService { private readonly IAlertDialogService _alertDialogService = alertDialogService; - private readonly IApplicationRestartService _applicationRestartService = applicationRestartService; private readonly IAppTitleService _appTitleService = appTitleService; + private readonly IApplicationRestartService _applicationRestartService = applicationRestartService; private readonly IMainThreadService _mainThreadService = mainThreadService; private readonly IPackageDeploymentService _packageDeploymentService = packageDeploymentService; private readonly ITraceLogger _traceLogger = traceLogger; diff --git a/src/EventLogExpert.UI/Interfaces/IDeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/IDeploymentService.cs similarity index 72% rename from src/EventLogExpert.UI/Interfaces/IDeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/IDeploymentService.cs index 08dbfb8b..885101f0 100644 --- a/src/EventLogExpert.UI/Interfaces/IDeploymentService.cs +++ b/src/EventLogExpert.UI/Update/Deployment/IDeploymentService.cs @@ -1,7 +1,7 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Update.Deployment; public interface IDeploymentService { diff --git a/src/EventLogExpert.UI/Interfaces/IPackageDeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/IPackageDeploymentService.cs similarity index 82% rename from src/EventLogExpert.UI/Interfaces/IPackageDeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/IPackageDeploymentService.cs index b77753b7..4359da3b 100644 --- a/src/EventLogExpert.UI/Interfaces/IPackageDeploymentService.cs +++ b/src/EventLogExpert.UI/Update/Deployment/IPackageDeploymentService.cs @@ -1,11 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Options; using Windows.Foundation; using Windows.Management.Deployment; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Update.Deployment; public interface IPackageDeploymentService { diff --git a/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs b/src/EventLogExpert.UI/Update/Deployment/PackageDeploymentOptions.cs similarity index 55% rename from src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs rename to src/EventLogExpert.UI/Update/Deployment/PackageDeploymentOptions.cs index bf40cb3c..53fdccf4 100644 --- a/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs +++ b/src/EventLogExpert.UI/Update/Deployment/PackageDeploymentOptions.cs @@ -1,12 +1,18 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Options; +namespace EventLogExpert.UI.Update.Deployment; /// Options for package deployment operations. -/// Allows updating from any version of the package, even if it would be a downgrade. +/// +/// Allows updating from any version of the package, even if it would be a +/// downgrade. +/// /// Forces the target application to shut down before the update is applied. -/// Defers the package registration until the application is no longer in use. +/// +/// Defers the package registration until the application is no longer +/// in use. +/// public sealed record PackageDeploymentOptions( bool ForceUpdateFromAnyVersion = false, bool ForceTargetAppShutdown = false, diff --git a/src/EventLogExpert.UI/Services/PackageDeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/PackageDeploymentService.cs similarity index 88% rename from src/EventLogExpert.UI/Services/PackageDeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/PackageDeploymentService.cs index fae7ad0f..f279568a 100644 --- a/src/EventLogExpert.UI/Services/PackageDeploymentService.cs +++ b/src/EventLogExpert.UI/Update/Deployment/PackageDeploymentService.cs @@ -1,12 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Options; using Windows.Foundation; using Windows.Management.Deployment; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update.Deployment; public sealed class PackageDeploymentService : IPackageDeploymentService { diff --git a/src/EventLogExpert.UI/Models/GitReleaseModel.cs b/src/EventLogExpert.UI/Update/GitHubRelease.cs similarity index 84% rename from src/EventLogExpert.UI/Models/GitReleaseModel.cs rename to src/EventLogExpert.UI/Update/GitHubRelease.cs index 1032e7f4..83f9db4e 100644 --- a/src/EventLogExpert.UI/Models/GitReleaseModel.cs +++ b/src/EventLogExpert.UI/Update/GitHubRelease.cs @@ -1,12 +1,12 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using System.Text.Json.Serialization; using System.Text.RegularExpressions; -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Update; -public readonly partial record struct GitReleaseModel +public readonly partial record struct GitHubRelease { [JsonPropertyName("name")] public string Version { get; init; } @@ -14,7 +14,7 @@ public readonly partial record struct GitReleaseModel [JsonPropertyName("published_at")] public DateTime ReleaseDate { get; init; } - [JsonPropertyName("assets")] public List Assets { get; init; } + [JsonPropertyName("assets")] public List Assets { get; init; } [JsonPropertyName("body")] public string RawChanges { get; init; } @@ -49,7 +49,7 @@ public List Changes private static partial Regex SplitChangeLog(); } -public readonly record struct GitReleaseAsset +public readonly record struct GitHubReleaseAsset { [JsonPropertyName("name")] public required string Name { get; init; } diff --git a/src/EventLogExpert.UI/Services/ReleaseNotesNormalizer.cs b/src/EventLogExpert.UI/Update/GitHubReleaseNormalizer.cs similarity index 52% rename from src/EventLogExpert.UI/Services/ReleaseNotesNormalizer.cs rename to src/EventLogExpert.UI/Update/GitHubReleaseNormalizer.cs index a98e49ea..f0aadd7f 100644 --- a/src/EventLogExpert.UI/Services/ReleaseNotesNormalizer.cs +++ b/src/EventLogExpert.UI/Update/GitHubReleaseNormalizer.cs @@ -1,25 +1,21 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Markdown; using System.Text.RegularExpressions; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update; /// -/// Normalizes a GitHub release body into a clean Markdown document that can be -/// rendered by . -/// -/// Future releases are hand-authored in rich Markdown (headings, bold, lists, -/// links). Hand-authored content largely passes through; the only rewrites -/// applied universally are converting * bullet markers to - for -/// consistency and collapsing runs of 3+ blank lines. Legacy releases used a -/// flat list of * <commit-id> description bullets, sometimes -/// wrapped in raw HTML (<details>/<summary>/ -/// <b>) and followed by an auto-generated build-pipeline footer. -/// This normalizer strips that noise so the renderer (which escapes raw HTML) -/// does not surface the markup as literal text. +/// Normalizes a GitHub release body into a clean Markdown document that can be rendered by +/// . Future releases are hand-authored in rich Markdown (headings, bold, lists, links). +/// Hand-authored content largely passes through; the only rewrites applied universally are converting * bullet +/// markers to - for consistency and collapsing runs of 3+ blank lines. Legacy releases used a flat list of +/// * <commit-id> description bullets, sometimes wrapped in raw HTML (<details>/ +/// <summary>/ <b>) and followed by an auto-generated build-pipeline footer. This normalizer +/// strips that noise so the renderer (which escapes raw HTML) does not surface the markup as literal text. /// -public static partial class ReleaseNotesNormalizer +internal static partial class GitHubReleaseNormalizer { public static string Normalize(string? rawBody) { @@ -53,11 +49,9 @@ public static string Normalize(string? rawBody) private static partial Regex AutoGeneratedFooterRegex(); /// - /// Matches a * bullet whose content optionally begins with a commit - /// identifier — either a 40-character SHA or a Markdown link - /// (e.g. [abc1234](https://...)) — and rewrites it as a clean - /// - description bullet. Bullets without a commit prefix are simply - /// converted from * to - style for consistency. + /// Matches a * bullet whose content optionally begins with a commit identifier — either a 40-character + /// SHA or a Markdown link (e.g. [abc1234](https://...)) — and rewrites it as a clean - description + /// bullet. Bullets without a commit prefix are simply converted from * to - style for consistency. /// [GeneratedRegex(@"^\*\s+(?:\[[0-9a-f]{6,}\]\([^)\s]+\)\s+|[0-9a-f]{40}\s+)?(.+)$", RegexOptions.Multiline | RegexOptions.IgnoreCase)] @@ -67,19 +61,17 @@ public static string Normalize(string? rawBody) private static partial Regex CollapseBlankLinesRegex(); /// - /// Strips the structural HTML wrappers and inline emphasis tags that - /// historically appeared in GitHub release bodies. Inner text is preserved. + /// Strips the structural HTML wrappers and inline emphasis tags that historically appeared in GitHub release + /// bodies. Inner text is preserved. /// [GeneratedRegex(@"]*>", RegexOptions.IgnoreCase)] private static partial Regex LegacyHtmlTagRegex(); /// - /// Captures a <summary> block and promotes its inner text to a - /// section heading so the collapsible "See More" label is preserved as a - /// visible divider in the rendered notes. Any nested HTML tags inside the - /// captured text are removed by before the - /// heading is emitted, so even non-legacy markup (e.g. <iframe>) - /// cannot surface as literal text in the heading. + /// Captures a <summary> block and promotes its inner text to a section heading so the collapsible + /// "See More" label is preserved as a visible divider in the rendered notes. Any nested HTML tags inside the captured + /// text are removed by before the heading is emitted, so even non-legacy markup (e.g. + /// <iframe>) cannot surface as literal text in the heading. /// [GeneratedRegex(@"]*>(.*?)", RegexOptions.IgnoreCase | RegexOptions.Singleline)] private static partial Regex SummaryTagRegex(); diff --git a/src/EventLogExpert.UI/Services/GitHubService.cs b/src/EventLogExpert.UI/Update/GitHubService.cs similarity index 83% rename from src/EventLogExpert.UI/Services/GitHubService.cs rename to src/EventLogExpert.UI/Update/GitHubService.cs index f3981b84..2fef1391 100644 --- a/src/EventLogExpert.UI/Services/GitHubService.cs +++ b/src/EventLogExpert.UI/Update/GitHubService.cs @@ -2,22 +2,16 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Models; using System.Text.Json; -namespace EventLogExpert.UI.Services; - -public interface IGitHubService -{ - Task> GetReleases(); -} +namespace EventLogExpert.UI.Update; public sealed class GitHubService(HttpClient httpClient, ITraceLogger traceLogger) : IGitHubService { private readonly HttpClient _httpClient = httpClient; private readonly ITraceLogger _traceLogger = traceLogger; - public async Task> GetReleases() + public async Task> GetReleases() { var response = await _httpClient.GetAsync("/repos/microsoft/EventLogExpert/releases"); @@ -31,7 +25,7 @@ public async Task> GetReleases() _traceLogger.Debug($"{nameof(GetReleases)} Attempt to retrieve {response.RequestMessage?.RequestUri} succeeded: {response.StatusCode}."); var stream = await response.Content.ReadAsStreamAsync(); - var content = await JsonSerializer.DeserializeAsync>(stream); + var content = await JsonSerializer.DeserializeAsync>(stream); if (content is not null) { return content; } diff --git a/src/EventLogExpert.UI/Update/IGitHubService.cs b/src/EventLogExpert.UI/Update/IGitHubService.cs new file mode 100644 index 00000000..cf458860 --- /dev/null +++ b/src/EventLogExpert.UI/Update/IGitHubService.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Update; + +public interface IGitHubService +{ + Task> GetReleases(); +} diff --git a/src/EventLogExpert.UI/Update/IUpdateService.cs b/src/EventLogExpert.UI/Update/IUpdateService.cs new file mode 100644 index 00000000..8e3dd1e9 --- /dev/null +++ b/src/EventLogExpert.UI/Update/IUpdateService.cs @@ -0,0 +1,13 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Update.ReleaseNotes; + +namespace EventLogExpert.UI.Update; + +public interface IUpdateService +{ + Task CheckForUpdates(bool usePreRelease, bool userInitiated = false); + + Task GetReleaseNotes(); +} diff --git a/src/EventLogExpert.UI/Update/ReleaseNotes/ReleaseNotesContent.cs b/src/EventLogExpert.UI/Update/ReleaseNotes/ReleaseNotesContent.cs new file mode 100644 index 00000000..70c3dfdc --- /dev/null +++ b/src/EventLogExpert.UI/Update/ReleaseNotes/ReleaseNotesContent.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Update.ReleaseNotes; + +public readonly record struct ReleaseNotesContent(string Title, string Markdown); diff --git a/src/EventLogExpert.UI/Services/UpdateService.cs b/src/EventLogExpert.UI/Update/UpdateService.cs similarity index 90% rename from src/EventLogExpert.UI/Services/UpdateService.cs rename to src/EventLogExpert.UI/Update/UpdateService.cs index 38bf9cfd..8629832f 100644 --- a/src/EventLogExpert.UI/Services/UpdateService.cs +++ b/src/EventLogExpert.UI/Update/UpdateService.cs @@ -2,19 +2,13 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Alerts; +using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Versioning; +using EventLogExpert.UI.Update.Deployment; +using EventLogExpert.UI.Update.ReleaseNotes; -namespace EventLogExpert.UI.Services; - -public readonly record struct ReleaseNotesContent(string Title, string Markdown); - -public interface IUpdateService -{ - Task CheckForUpdates(bool usePreRelease, bool userInitiated = false); - - Task GetReleaseNotes(); -} +namespace EventLogExpert.UI.Update; public sealed class UpdateService( ICurrentVersionProvider versionProvider, @@ -53,13 +47,13 @@ await alertDialogService.ShowAlert("Update Check Unavailable", return; } - GitReleaseModel? latest = null; + GitHubRelease? latest = null; try { // Versions are based on current DateTime so this is safer than dealing with // stripping the v off the Version for every release - GitReleaseModel[] releases = [.. (await githubService.GetReleases()).OrderByDescending(x => x.ReleaseDate)]; + GitHubRelease[] releases = [.. (await githubService.GetReleases()).OrderByDescending(x => x.ReleaseDate)]; if (releases.Length <= 0) { @@ -178,7 +172,7 @@ await alertDialogService.ShowAlert("Release Notes Failure", return null; } - var markdown = ReleaseNotesNormalizer.Normalize(_currentRawChanges); + var markdown = GitHubReleaseNormalizer.Normalize(_currentRawChanges); var title = $"Release notes for v{versionProvider.CurrentVersion}"; return new ReleaseNotesContent(title, markdown); diff --git a/src/EventLogExpert/App.xaml.cs b/src/EventLogExpert/App.xaml.cs index e811c091..20abf975 100644 --- a/src/EventLogExpert/App.xaml.cs +++ b/src/EventLogExpert/App.xaml.cs @@ -3,11 +3,9 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; -using EventLogExpert.UI.Store.EventLog; +using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Settings; using Fluxor; using System.Collections.Immutable; using Application = Microsoft.Maui.Controls.Application; diff --git a/src/EventLogExpert/Components/Layout/MainLayout.razor b/src/EventLogExpert/Components/Layout/MainLayout.razor index 53045bb4..92d50e95 100644 --- a/src/EventLogExpert/Components/Layout/MainLayout.razor +++ b/src/EventLogExpert/Components/Layout/MainLayout.razor @@ -10,7 +10,7 @@
- +
diff --git a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs index 5aeab89d..428a93e4 100644 --- a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs +++ b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs @@ -2,8 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.Services; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Settings; +using EventLogExpert.UI.Update; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs b/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs index 9df2fba0..f97d4e3d 100644 --- a/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs +++ b/src/EventLogExpert/Components/Layout/UnhandledExceptionHandler.razor.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Banner; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index 25041eb3..2304acd5 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -4,8 +4,9 @@ using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Store.EventLog; +using EventLogExpert.UI.Common.Preferences; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Settings; using Fluxor; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -21,9 +22,11 @@ public sealed partial class DetailsPane private bool _hasOpened; private bool _isVisible; private bool _isXmlVisible; - /// Resolved XML for the currently selected event. null means a fetch is in flight - /// (show "Resolving XML..."); empty string means resolved-with-no-content (e.g. live-watcher event - /// without a record id, or render failure); any other value is the rendered XML. + /// + /// Resolved XML for the currently selected event. null means a fetch is in flight (show "Resolving + /// XML..."); empty string means resolved-with-no-content (e.g. live-watcher event without a record id, or render + /// failure); any other value is the rendered XML. + /// private string? _resolvedXml; private ResolvedEvent? _selectedEvent; /// Cancels any in-flight XML resolution when the selection changes again before completion. diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor b/src/EventLogExpert/Components/Sections/FilterPane.razor index 61ebc392..85ba6472 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor @@ -1,4 +1,3 @@ -@using EventLogExpert.UI @inherits FluxorComponent
diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs index cc66c7d2..44933434 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs @@ -2,11 +2,12 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Modals.Filters; -using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.EventLog; -using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Common.Display; +using EventLogExpert.UI.EventLog; +using EventLogExpert.UI.Filter; +using EventLogExpert.UI.FilterPane; +using EventLogExpert.UI.Modal; +using EventLogExpert.UI.Settings; using Fluxor; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; @@ -16,8 +17,8 @@ namespace EventLogExpert.Components.Sections; public sealed partial class FilterPane : IDisposable { - private readonly FilterDateModel _model = new(); - private readonly List _pendingDrafts = []; + private readonly DateFilter _model = new(); + private readonly List _pendingDrafts = []; private bool _canEditDate; private TimeZoneInfo _currentTimeZone = TimeZoneInfo.Utc; @@ -29,8 +30,6 @@ public sealed partial class FilterPane : IDisposable [Inject] private IState FilterPaneState { get; init; } = null!; - [Inject] private IModalService ModalService { get; init; } = null!; - private bool HasFilters => IsDateFilterVisible || FilterPaneState.Value.Filters.IsEmpty is false || _pendingDrafts.Count > 0; @@ -38,21 +37,23 @@ public sealed partial class FilterPane : IDisposable private string MenuState => HasFilters ? _isFilterListVisible.ToString().ToLower() : "false"; + [Inject] private IModalService ModalService { get; init; } = null!; + [Inject] private ISettingsService Settings { get; init; } = null!; public void Dispose() => Settings.TimeZoneChanged -= UpdateFilterDateTimeZone; protected override void OnInitialized() { - SubscribeToAction(action => + SubscribeToAction(action => { _canEditDate = false; _pendingDrafts.Clear(); }); - SubscribeToAction(action => + SubscribeToAction(action => { - UpdateFilterDate(action.FilterDateModel); + UpdateFilterDate(action.DateFilter); }); Settings.TimeZoneChanged += UpdateFilterDateTimeZone; @@ -62,13 +63,13 @@ protected override void OnInitialized() private void AddAdvancedFilter() { - _pendingDrafts.Add(new FilterDraftModel { FilterType = FilterType.Advanced }); + _pendingDrafts.Add(new FilterDraft { FilterType = FilterType.Advanced }); _isFilterListVisible = true; } private void AddBasicFilter() { - _pendingDrafts.Add(new FilterDraftModel { FilterType = FilterType.Basic }); + _pendingDrafts.Add(new FilterDraft { FilterType = FilterType.Basic }); _isFilterListVisible = true; } @@ -91,15 +92,15 @@ private void AddDateFilter() private void AddExclusion() { - _pendingDrafts.Add(new FilterDraftModel { FilterType = FilterType.Basic, IsExcluded = true }); + _pendingDrafts.Add(new FilterDraft { FilterType = FilterType.Basic, IsExcluded = true }); _isFilterListVisible = true; } private void ApplyDateFilter() { Dispatcher.Dispatch( - new FilterPaneAction.SetFilterDateRange( - new FilterDateModel + new SetFilterDateRangeAction( + new DateFilter { After = _model.After?.ConvertTimeZoneToUtc(_currentTimeZone), Before = _model.Before?.ConvertTimeZoneToUtc(_currentTimeZone) @@ -128,25 +129,25 @@ private void HandleKeyDown(KeyboardEventArgs e) } } - private void HandlePendingDiscard(FilterDraftModel draft) => _pendingDrafts.Remove(draft); + private void HandlePendingDiscard(FilterDraft draft) => _pendingDrafts.Remove(draft); - private void HandlePendingSave(FilterDraftModel draft, FilterModel filter) + private void HandlePendingSave(FilterDraft draft, SavedFilter filter) { _pendingDrafts.Remove(draft); - Dispatcher.Dispatch(new FilterPaneAction.SetFilter(filter)); + Dispatcher.Dispatch(new SetFilterAction(filter)); } private void RemoveDateFilter() { _canEditDate = false; - Dispatcher.Dispatch(new FilterPaneAction.SetFilterDateRange(null)); + Dispatcher.Dispatch(new SetFilterDateRangeAction(null)); } - private void ToggleDateFilter() => Dispatcher.Dispatch(new FilterPaneAction.ToggleFilterDate()); + private void ToggleDateFilter() => Dispatcher.Dispatch(new ToggleFilterDateAction()); private void ToggleMenu() => _isFilterListVisible = !_isFilterListVisible; - private void UpdateFilterDate(FilterDateModel? updatedDate) + private void UpdateFilterDate(DateFilter? updatedDate) { _model.Before = updatedDate?.Before?.ConvertTimeZone(_currentTimeZone); _model.After = updatedDate?.After?.ConvertTimeZone(_currentTimeZone); diff --git a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor b/src/EventLogExpert/Components/Sections/LogTabBar.razor similarity index 89% rename from src/EventLogExpert/Components/Sections/SplitLogTabPane.razor rename to src/EventLogExpert/Components/Sections/LogTabBar.razor index 31f4a77d..7804c784 100644 --- a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor +++ b/src/EventLogExpert/Components/Sections/LogTabBar.razor @@ -1,6 +1,6 @@ @inherits FluxorComponent -