From 44cd0288b7bf5d76c6297ca527473fa302deac10 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 17:11:25 -0500 Subject: [PATCH 01/33] Extract Modal/Alert broker, move System/File/Clipboard to Common --- .../Base/ModalBase.cs | 29 +- .../Base/ModalChrome.razor.cs | 22 +- .../Database/DatabaseEntryRow.razor.cs | 8 +- .../Database/DatabaseRecoveryDialog.razor | 1 - .../Database/DatabaseRecoveryHost.razor.cs | 17 +- .../Filters/AdvancedFilterRow.razor.cs | 8 +- .../Filters/Base/EditableFilterRowBase.cs | 6 +- .../Filters/FilterCacheRow.razor.cs | 8 +- .../Filters/FilterCategoryEditor.razor | 1 - .../Filters/FilterGroupRow.razor.cs | 6 +- .../Filters/FilterRow.razor.cs | 8 +- .../Inputs/BooleanSelect.razor | 1 - .../Inputs/ValueSelect.razor.cs | 20 +- .../Menu/MenuRenderer.razor.cs | 14 +- .../Modals/DebugLogModal.razor | 2 +- .../Modals/DebugLogModal.razor.cs | 3 +- .../Modals/Filters/FilterCacheModal.razor.cs | 13 +- .../Modals/Filters/FilterGroup.razor.cs | 22 +- .../Modals/Filters/FilterGroupModal.razor | 1 - .../Modals/Filters/FilterGroupModal.razor.cs | 9 +- .../Modals/Filters/FilterGroupSection.razor | 2 - .../Modals/SettingsModal.razor.cs | 17 +- .../MtaProviderSource.cs | 28 +- .../ProviderSource.cs | 35 +- src/EventLogExpert.EventDbTool/RegexHelper.cs | 16 +- .../Common/Channels/LogChannelMethods.cs | 5 +- .../Common/Databases/DatabasePathSorter.cs | 20 +- .../Common/Events/ResolvedEvent.cs | 8 +- .../Interop/NativeErrorResolver.cs | 14 +- .../Interop/NativeMethods.cs | 12 +- .../Interop/Win32ErrorCodes.cs | 32 +- .../Providers/ProviderDetails.cs | 7 +- .../Providers/RegistryProvider.cs | 7 +- .../Readers/EventLogReader.cs | 13 +- .../Readers/EventLogWatcher.cs | 12 +- .../Resolvers/EventResolver.cs | 6 +- .../Resolvers/EventResolverBase.cs | 37 +- .../Resolvers/IEventXmlResolver.cs | 14 +- .../Common/Files/FilePickerFileTypes.cs | 10 + .../Common/Files/FileSaveFileTypes.cs | 17 + .../Files}/IFilePickerService.cs | 12 +- .../Common/Files/IFileSaveService.cs | 34 + .../{ => Defaults}/ColumnDefaults.cs | 2 +- .../{ => Defaults}/DatabaseStatusLabels.cs | 0 .../{ => Defaults}/DateRangeDefaults.cs | 2 +- src/EventLogExpert.UI/Enums.cs | 125 --- src/EventLogExpert.UI/Enums/CacheType.cs | 10 + src/EventLogExpert.UI/Enums/ColumnName.cs | 22 + src/EventLogExpert.UI/Enums/CopyType.cs | 12 + src/EventLogExpert.UI/Enums/DatabaseStatus.cs | 15 + src/EventLogExpert.UI/Enums/FilterCategory.cs | 21 + .../Enums/FilterEvaluator.cs | 15 + src/EventLogExpert.UI/Enums/FilterType.cs | 11 + src/EventLogExpert.UI/Enums/HighlightColor.cs | 36 + src/EventLogExpert.UI/Enums/OpenLogStatus.cs | 12 + src/EventLogExpert.UI/Enums/Theme.cs | 11 + .../EventLogExpert.UI.csproj | 5 + .../DisplayExtensions.cs} | 2 +- .../{ => Extensions}/FilterMethods.cs | 2 +- .../Interfaces/IClipboardService.cs | 10 +- .../Interfaces/IFileSaveService.cs | 45 - .../Interfaces/IInlineAlertHost.cs | 15 - .../Interfaces/IMenuActionService.cs | 4 +- .../Interfaces/InlineAlertRequest.cs | 16 + .../Interfaces/InlineAlertResult.cs | 7 + .../Models/{ => Banner}/BannerInfoEntry.cs | 0 .../{ => Banner}/BannerProgressEntry.cs | 0 .../Models/{ => Banner}/BannerSeverity.cs | 0 .../Models/{ => Banner}/ErrorBannerEntry.cs | 0 .../Models/{ => Database}/DatabaseEntry.cs | 0 .../Models/{ => Database}/ImportFailure.cs | 0 .../Models/{ => Database}/ImportResult.cs | 0 .../Models/{ => DebugLog}/DebugLogEntry.cs | 0 .../Models/{ => EventLog}/EventLogData.cs | 0 .../Models/{ => EventLog}/EventLogId.cs | 0 .../Models/{ => EventLog}/EventTableModel.cs | 0 .../Models/{ => Filter}/BasicFilter.cs | 0 .../Models/{ => Filter}/CompiledFilter.cs | 0 .../Models/{ => Filter}/EventFilter.cs | 5 +- .../Models/{ => Filter}/FilterData.cs | 8 +- .../Models/{ => Filter}/FilterDataDraft.cs | 0 .../Models/{ => Filter}/FilterDateModel.cs | 0 .../Models/{ => Filter}/FilterDraftModel.cs | 0 .../Models/{ => Filter}/FilterGroupData.cs | 0 .../Models/{ => Filter}/FilterGroupId.cs | 0 .../Models/{ => Filter}/FilterGroupModel.cs | 0 .../Models/{ => Filter}/FilterId.cs | 0 .../Models/{ => Filter}/FilterModel.cs | 4 +- .../{ => Filter}/FilterModelJsonConverter.cs | 0 .../Models/{ => Filter}/SubFilter.cs | 0 .../Models/{ => Filter}/SubFilterDraft.cs | 0 .../Models/{ => Menu}/MenuItem.cs | 10 +- .../Models/{ => Update}/GitReleaseModel.cs | 0 .../UpgradeBatchCompletedEventArgs.cs | 0 .../UpgradeBatchProgressEventArgs.cs | 0 .../{ => Upgrade}/UpgradeBatchResult.cs | 0 .../UpgradeBatchStartedEventArgs.cs | 0 .../Models/{ => Upgrade}/UpgradeFailure.cs | 0 .../Models/{ => Upgrade}/UpgradePhase.cs | 0 .../{ => Upgrade}/UpgradeProgressScope.cs | 0 .../Options/FileLocationOptions.cs | 2 +- .../Options/PackageDeploymentOptions.cs | 10 +- .../Services/AlertPresentation.cs | 35 - .../Services/Alerts/AlertPresentation.cs | 32 + .../{ => Alerts}/EmptyLogAlertFormatter.cs | 4 +- .../Alerts}/IAlertDialogService.cs | 0 .../Services/Alerts/IInlineAlertHostBroker.cs | 30 + .../Services/Alerts/InlineAlertHostBroker.cs | 57 ++ .../{ => Alerts}/ModalAlertDialogService.cs | 8 +- .../Services/{ => Banner}/BannerService.cs | 0 .../{ => Banner}/BannerViewSelector.cs | 0 .../Banner}/IBannerService.cs | 0 .../{ => Database}/DatabaseService.cs | 0 .../Database}/IDatabaseService.cs | 0 .../{ => DebugLog}/DebugLogEntryParser.cs | 0 .../{ => DebugLog}/DebugLogProjection.cs | 0 .../{ => DebugLog}/DebugLogService.cs | 0 .../DebugLog}/IFileLogger.cs | 0 .../{ => Deployment}/DeploymentService.cs | 4 +- .../Deployment}/IDeploymentService.cs | 0 .../Deployment}/IPackageDeploymentService.cs | 0 .../PackageDeploymentService.cs | 0 .../Display}/DisplayConverter.cs | 2 +- .../{ => Display}/ReversedListView.cs | 0 .../{ => Filter}/FilterCategoryItemsCache.cs | 6 +- .../Services/{ => Filter}/FilterCompiler.cs | 0 .../Services/{ => Filter}/FilterService.cs | 0 .../Filter}/IFilterService.cs | 0 .../Services/MainThreadService.cs | 44 - .../Menu}/IMenuService.cs | 0 .../Services/{ => Menu}/MenuService.cs | 0 .../Modal}/IModalService.cs | 8 - .../Services/{ => Modal}/ModalService.cs | 54 +- .../ReleaseNotesMarkdownRenderer.cs | 5 +- .../ReleaseNotesNormalizer.cs | 43 +- .../Settings}/ISettingsService.cs | 0 .../{ => Settings}/SettingsService.cs | 0 .../Services/{ => System}/AppTitleService.cs | 9 - .../Services/System/IAppTitleService.cs | 13 + .../System}/IApplicationRestartService.cs | 0 .../Services/System/IMainThreadService.cs | 12 + .../System}/ITitleProvider.cs | 0 .../System}/IWindowsIdentityProvider.cs | 0 .../Services/{ => Update}/GitHubService.cs | 5 - .../Services/Update/IGitHubService.cs | 11 + .../Services/Update/IUpdateService.cs | 11 + .../Services/Update/ReleaseNotesContent.cs | 6 + .../Services/{ => Update}/UpdateService.cs | 9 - .../CurrentVersionProvider.cs | 11 - .../Versioning/ICurrentVersionProvider.cs | 15 + .../Versioning}/IPackageVersionProvider.cs | 0 .../PackageVersionProvider.cs | 2 +- .../Store/EventLog/AddEventAction.cs | 8 + .../Store/EventLog/AddEventBufferedAction.cs | 8 + .../Store/EventLog/AddEventSuccessAction.cs | 9 + .../Store/EventLog/CloseAllAction.cs | 6 + .../Store/EventLog/CloseLogAction.cs | 8 + .../{EventLogEffects.cs => Effects.cs} | 148 ++-- .../Store/EventLog/EventLogAction.cs | 56 -- .../Store/EventLog/ILogReloadCoordinator.cs | 27 +- .../Store/EventLog/ILogWatcherService.cs | 13 + .../Store/EventLog/LoadEventsAction.cs | 9 + .../Store/EventLog/LoadEventsPartialAction.cs | 9 + .../Store/EventLog/LoadNewEventsAction.cs | 6 + .../Store/EventLog/LogReopenInfo.cs | 8 + .../Store/EventLog/LogReopenSnapshot.cs | 23 + .../Store/EventLog/LogWatcherService.cs | 11 +- .../Store/EventLog/OpenLogAction.cs | 8 + .../{EventLogReducers.cs => Reducers.cs} | 26 +- .../Store/EventLog/SelectEventAction.cs | 11 + .../Store/EventLog/SelectEventsAction.cs | 8 + .../EventLog/SetContinuouslyUpdateAction.cs | 6 + .../Store/EventLog/SetFiltersAction.cs | 8 + .../Store/EventLog/SetSelectedEventsAction.cs | 24 + .../Store/EventTable/AddTableAction.cs | 8 + .../EventTable/AppendTableEventsAction.cs | 9 + .../AppendTableEventsBatchAction.cs | 9 + .../Store/EventTable/CloseAllAction.cs | 6 + .../Store/EventTable/CloseLogAction.cs | 8 + .../{EventTableEffects.cs => Effects.cs} | 18 +- .../Store/EventTable/EventTableAction.cs | 49 -- .../Store/EventTable/LoadColumnsAction.cs | 6 + .../EventTable/LoadColumnsCompletedAction.cs | 11 + .../{EventTableReducers.cs => Reducers.cs} | 30 +- .../Store/EventTable/ReorderColumnAction.cs | 6 + .../EventTable/ResetColumnDefaultsAction.cs | 6 + .../Store/EventTable/SetActiveTableAction.cs | 8 + .../Store/EventTable/SetColumnWidthAction.cs | 6 + .../Store/EventTable/SetOrderByAction.cs | 6 + .../Store/EventTable/ToggleColumnAction.cs | 6 + .../Store/EventTable/ToggleLoadingAction.cs | 8 + .../Store/EventTable/ToggleSortingAction.cs | 6 + .../EventTable/UpdateDisplayedEventsAction.cs | 10 + .../Store/EventTable/UpdateTableAction.cs | 9 + .../FilterCache/AddFavoriteFilterAction.cs | 6 + .../AddFavoriteFilterCompletedAction.cs | 8 + .../FilterCache/AddRecentFilterAction.cs | 6 + .../AddRecentFilterCompletedAction.cs | 8 + .../{FilterCacheEffects.cs => Effects.cs} | 24 +- .../Store/FilterCache/FilterCacheAction.cs | 31 - .../FilterCache/ImportFavoritesAction.cs | 6 + .../Store/FilterCache/LoadFiltersAction.cs | 6 + .../FilterCache/LoadFiltersCompletedAction.cs | 10 + .../{FilterCacheReducers.cs => Reducers.cs} | 12 +- .../FilterCache/RemoveFavoriteFilterAction.cs | 6 + .../RemoveFavoriteFilterCompletedAction.cs | 10 + .../Store/FilterGroup/AddGroupAction.cs | 8 + .../{FilterGroupEffects.cs => Effects.cs} | 16 +- .../Store/FilterGroup/FilterGroupAction.cs | 29 - .../Store/FilterGroup/ImportGroupsAction.cs | 8 + .../Store/FilterGroup/LoadGroupsAction.cs | 6 + .../FilterGroup/LoadGroupsSuccessAction.cs | 8 + .../{FilterGroupReducers.cs => Reducers.cs} | 22 +- .../Store/FilterGroup/RemoveFilterAction.cs | 8 + .../Store/FilterGroup/RemoveGroupAction.cs | 8 + .../Store/FilterGroup/SetFilterAction.cs | 8 + .../Store/FilterGroup/SetGroupAction.cs | 8 + .../FilterGroup/ToggleFilterExcludedAction.cs | 8 + .../Store/FilterGroup/ToggleGroupAction.cs | 8 + .../Store/FilterPane/AddFilterAction.cs | 8 + .../FilterPane/ApplyFilterGroupAction.cs | 8 + .../Store/FilterPane/ClearAllFiltersAction.cs | 6 + .../{FilterPaneEffects.cs => Effects.cs} | 40 +- .../Store/FilterPane/FilterPaneAction.cs | 35 - .../{FilterPaneReducers.cs => Reducers.cs} | 32 +- .../Store/FilterPane/RemoveFilterAction.cs | 8 + .../Store/FilterPane/SaveFilterGroupAction.cs | 6 + .../Store/FilterPane/SetFilterAction.cs | 8 + .../FilterPane/SetFilterDateRangeAction.cs | 8 + .../SetFilterDateRangeSuccessAction.cs | 8 + .../Store/FilterPane/SetIsLoadingAction.cs | 6 + .../FilterPane/ToggleFilterDateAction.cs | 6 + .../FilterPane/ToggleFilterEnabledAction.cs | 8 + .../FilterPane/ToggleFilterExcludedAction.cs | 8 + .../Store/FilterPane/ToggleIsEnabledAction.cs | 6 + .../Store/LoggingMiddleware.cs | 26 +- .../Store/StatusBar/ClearStatusAction.cs | 6 + .../Store/StatusBar/CloseAllAction.cs | 6 + .../{StatusBarReducers.cs => Reducers.cs} | 12 +- .../Store/StatusBar/SetEventsLoadingAction.cs | 12 + .../StatusBar/SetResolverStatusAction.cs | 6 + .../Store/StatusBar/StatusBarAction.cs | 21 - src/EventLogExpert/App.xaml.cs | 1 - .../Components/Sections/DetailsPane.razor.cs | 8 +- .../Components/Sections/EventTable.razor.cs | 34 +- .../Components/Sections/FilterPane.razor | 3 +- .../Components/Sections/FilterPane.razor.cs | 16 +- .../Sections/SplitLogTabPane.razor.cs | 6 +- .../Components/Sections/StatusBar.razor | 1 - src/EventLogExpert/EventLogExpert.csproj | 1 + .../Interop}/NativeMethods.cs | 19 +- src/EventLogExpert/Interop/RestartFlags.cs | 25 + src/EventLogExpert/MainPage.xaml.cs | 7 +- src/EventLogExpert/MauiProgram.cs | 17 +- .../Platforms/Windows/App.xaml.cs | 10 +- .../Platforms/Windows/FolderPickerHelper.cs | 6 +- .../Services/ClipboardService.cs | 3 +- .../Services/KeyboardShortcutService.cs | 52 +- .../Services/MauiFilePickerService.cs | 2 +- .../Services/MauiFileSaveService.cs | 2 +- .../Services/MauiMainThreadService.cs | 24 + .../Services/MauiMenuActionService.cs | 28 +- .../WindowsApplicationRestartService.cs} | 11 +- .../Services/WindowsIdentityProvider.cs | 4 +- .../GlobalUsings.cs | 3 + .../Readers/SmallEvtxFixture.cs | 5 +- .../TestUtils/Constants/Constants.Provider.cs | 4 +- .../BannerHostTests.cs | 66 +- .../Database/DatabaseEntryRowTests.cs | 32 +- .../Database/DatabaseRecoveryDialogTests.cs | 78 +- .../Database/DatabaseRecoveryHostTests.cs | 3 +- .../SettingsUpgradeProgressBannerTests.cs | 5 +- .../GlobalUsings.cs | 3 + .../Modals/DebugLogModalTests.cs | 173 ++-- .../GlobalUsings.cs | 3 + .../TestUtils/Constants/Constants.Database.cs | 2 +- .../Databases/DatabasePathSorterTests.cs | 26 +- .../GlobalUsings.cs | 3 + .../Interop/NativeMethodsEvtTests.cs | 124 +-- .../CompressedJsonValueConverterTests.cs | 3 +- .../ProviderDbContextTests.cs | 3 +- ...EventProviderDatabaseEventResolverTests.cs | 38 +- .../LocalProviderEventResolverTests.cs | 176 ++-- .../Resolvers/VersatileEventResolverTests.cs | 246 +++--- .../TestUtils/Constants/Constants.Provider.cs | 24 +- .../TestUtils/Constants/Constants.Resolver.cs | 26 +- .../DatabaseStatusLabelsTests.cs | 3 +- .../{ => Defaults}/DateRangeDefaultsTests.cs | 2 +- .../{ => Extensions}/FilterMethodsTests.cs | 2 +- .../EventLogExpert.UI.Tests/GlobalUsings.cs | 3 + .../{ => Filter}/FilterDataDraftTests.cs | 2 +- .../Models/{ => Filter}/FilterDataTests.cs | 2 +- .../{ => Filter}/FilterDraftModelTests.cs | 2 +- .../Models/{ => Filter}/FilterModelTests.cs | 2 +- .../Models/{ => Filter}/RequiresXmlTests.cs | 9 +- .../{ => Update}/GitReleaseModelTests.cs | 7 +- .../EmptyLogAlertFormatterTests.cs | 2 +- .../Alerts/InlineAlertHostBrokerTests.cs | 161 ++++ .../ModalAlertDialogServiceTests.cs | 152 ++-- .../{ => Banner}/BannerServiceTests.cs | 2 +- .../{ => Banner}/BannerViewSelectorTests.cs | 2 +- .../{ => Database}/DatabaseServiceTests.cs | 2 +- .../DebugLogEntryParserTests.cs | 50 +- .../{ => DebugLog}/DebugLogProjectionTests.cs | 258 +++--- .../{ => DebugLog}/DebugLogServiceTests.cs | 2 +- .../DeploymentServiceTests.cs | 54 +- .../{ => Display}/ReversedListViewTests.cs | 54 +- .../FilterCategoryItemsCacheTests.cs | 2 +- .../{ => Filter}/FilterCompilerTests.cs | 2 +- .../{ => Filter}/FilterServiceTests.cs | 2 +- .../Services/{ => Menu}/MenuServiceTests.cs | 2 +- .../Services/{ => Modal}/ModalServiceTests.cs | 124 +-- .../ReleaseNotesMarkdownRendererTests.cs | 38 +- .../ReleaseNotesNormalizerTests.cs | 2 +- .../{ => Settings}/SettingsServiceTests.cs | 146 ++-- .../{ => System}/AppTitleServiceTests.cs | 16 +- .../{ => Update}/GitHubServiceTests.cs | 4 +- .../{ => Update}/UpdateServiceTests.cs | 100 +-- ...ventLogEffectsTests.cs => EffectsTests.cs} | 304 +++---- .../Store/EventLog/EventLogStoreTests.cs | 300 +++---- ...ntTableEffectsTests.cs => EffectsTests.cs} | 46 +- .../Store/EventTable/EventTableStoreTests.cs | 818 +++--------------- ...erCacheEffectsTests.cs => EffectsTests.cs} | 90 +- .../FilterCache/FilterCacheStoreTests.cs | 136 +-- ...erGroupEffectsTests.cs => EffectsTests.cs} | 14 +- .../FilterGroup/FilterGroupStoreTests.cs | 148 ++-- ...terPaneEffectsTests.cs => EffectsTests.cs} | 115 ++- .../Store/FilterPane/FilterPaneStoreTests.cs | 224 ++--- .../Store/StatusBar/StatusBarStoreTests.cs | 124 +-- .../TestUtils/Constants/Constants.AppTitle.cs | 5 +- .../TestUtils/Constants/Constants.Database.cs | 16 +- .../TestUtils/Constants/Constants.DebugLog.cs | 2 +- .../Constants/Constants.Deployment.cs | 4 +- .../TestUtils/Constants/Constants.EventLog.cs | 7 +- .../TestUtils/Constants/Constants.Filter.cs | 54 +- .../Constants/Constants.GitHubRelease.cs | 5 +- .../TestUtils/Constants/Constants.Settings.cs | 4 +- .../TestUtils/GitHubUtils.cs | 9 +- 338 files changed, 3698 insertions(+), 3929 deletions(-) create mode 100644 src/EventLogExpert.UI/Common/Files/FilePickerFileTypes.cs create mode 100644 src/EventLogExpert.UI/Common/Files/FileSaveFileTypes.cs rename src/EventLogExpert.UI/{Interfaces => Common/Files}/IFilePickerService.cs (64%) create mode 100644 src/EventLogExpert.UI/Common/Files/IFileSaveService.cs rename src/EventLogExpert.UI/{ => Defaults}/ColumnDefaults.cs (97%) rename src/EventLogExpert.UI/{ => Defaults}/DatabaseStatusLabels.cs (100%) rename src/EventLogExpert.UI/{ => Defaults}/DateRangeDefaults.cs (98%) delete mode 100644 src/EventLogExpert.UI/Enums.cs create mode 100644 src/EventLogExpert.UI/Enums/CacheType.cs create mode 100644 src/EventLogExpert.UI/Enums/ColumnName.cs create mode 100644 src/EventLogExpert.UI/Enums/CopyType.cs create mode 100644 src/EventLogExpert.UI/Enums/DatabaseStatus.cs create mode 100644 src/EventLogExpert.UI/Enums/FilterCategory.cs create mode 100644 src/EventLogExpert.UI/Enums/FilterEvaluator.cs create mode 100644 src/EventLogExpert.UI/Enums/FilterType.cs create mode 100644 src/EventLogExpert.UI/Enums/HighlightColor.cs create mode 100644 src/EventLogExpert.UI/Enums/OpenLogStatus.cs create mode 100644 src/EventLogExpert.UI/Enums/Theme.cs rename src/EventLogExpert.UI/{ExtensionMethods.cs => Extensions/DisplayExtensions.cs} (95%) rename src/EventLogExpert.UI/{ => Extensions}/FilterMethods.cs (99%) delete mode 100644 src/EventLogExpert.UI/Interfaces/IFileSaveService.cs create mode 100644 src/EventLogExpert.UI/Interfaces/InlineAlertRequest.cs create mode 100644 src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs rename src/EventLogExpert.UI/Models/{ => Banner}/BannerInfoEntry.cs (100%) rename src/EventLogExpert.UI/Models/{ => Banner}/BannerProgressEntry.cs (100%) rename src/EventLogExpert.UI/Models/{ => Banner}/BannerSeverity.cs (100%) rename src/EventLogExpert.UI/Models/{ => Banner}/ErrorBannerEntry.cs (100%) rename src/EventLogExpert.UI/Models/{ => Database}/DatabaseEntry.cs (100%) rename src/EventLogExpert.UI/Models/{ => Database}/ImportFailure.cs (100%) rename src/EventLogExpert.UI/Models/{ => Database}/ImportResult.cs (100%) rename src/EventLogExpert.UI/Models/{ => DebugLog}/DebugLogEntry.cs (100%) rename src/EventLogExpert.UI/Models/{ => EventLog}/EventLogData.cs (100%) rename src/EventLogExpert.UI/Models/{ => EventLog}/EventLogId.cs (100%) rename src/EventLogExpert.UI/Models/{ => EventLog}/EventTableModel.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/BasicFilter.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/CompiledFilter.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/EventFilter.cs (90%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterData.cs (73%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterDataDraft.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterDateModel.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterDraftModel.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterGroupData.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterGroupId.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterGroupModel.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterId.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterModel.cs (95%) rename src/EventLogExpert.UI/Models/{ => Filter}/FilterModelJsonConverter.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/SubFilter.cs (100%) rename src/EventLogExpert.UI/Models/{ => Filter}/SubFilterDraft.cs (100%) rename src/EventLogExpert.UI/Models/{ => Menu}/MenuItem.cs (86%) rename src/EventLogExpert.UI/Models/{ => Update}/GitReleaseModel.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradeBatchCompletedEventArgs.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradeBatchProgressEventArgs.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradeBatchResult.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradeBatchStartedEventArgs.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradeFailure.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradePhase.cs (100%) rename src/EventLogExpert.UI/Models/{ => Upgrade}/UpgradeProgressScope.cs (100%) delete mode 100644 src/EventLogExpert.UI/Services/AlertPresentation.cs create mode 100644 src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs rename src/EventLogExpert.UI/Services/{ => Alerts}/EmptyLogAlertFormatter.cs (88%) rename src/EventLogExpert.UI/{Interfaces => Services/Alerts}/IAlertDialogService.cs (100%) create mode 100644 src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs create mode 100644 src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs rename src/EventLogExpert.UI/Services/{ => Alerts}/ModalAlertDialogService.cs (95%) rename src/EventLogExpert.UI/Services/{ => Banner}/BannerService.cs (100%) rename src/EventLogExpert.UI/Services/{ => Banner}/BannerViewSelector.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/Banner}/IBannerService.cs (100%) rename src/EventLogExpert.UI/Services/{ => Database}/DatabaseService.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/Database}/IDatabaseService.cs (100%) rename src/EventLogExpert.UI/Services/{ => DebugLog}/DebugLogEntryParser.cs (100%) rename src/EventLogExpert.UI/Services/{ => DebugLog}/DebugLogProjection.cs (100%) rename src/EventLogExpert.UI/Services/{ => DebugLog}/DebugLogService.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/DebugLog}/IFileLogger.cs (100%) rename src/EventLogExpert.UI/Services/{ => Deployment}/DeploymentService.cs (99%) rename src/EventLogExpert.UI/{Interfaces => Services/Deployment}/IDeploymentService.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/Deployment}/IPackageDeploymentService.cs (100%) rename src/EventLogExpert.UI/Services/{ => Deployment}/PackageDeploymentService.cs (100%) rename src/EventLogExpert.UI/{Models => Services/Display}/DisplayConverter.cs (94%) rename src/EventLogExpert.UI/Services/{ => Display}/ReversedListView.cs (100%) rename src/EventLogExpert.UI/Services/{ => Filter}/FilterCategoryItemsCache.cs (86%) rename src/EventLogExpert.UI/Services/{ => Filter}/FilterCompiler.cs (100%) rename src/EventLogExpert.UI/Services/{ => Filter}/FilterService.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/Filter}/IFilterService.cs (100%) delete mode 100644 src/EventLogExpert.UI/Services/MainThreadService.cs rename src/EventLogExpert.UI/{Interfaces => Services/Menu}/IMenuService.cs (100%) rename src/EventLogExpert.UI/Services/{ => Menu}/MenuService.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/Modal}/IModalService.cs (74%) rename src/EventLogExpert.UI/Services/{ => Modal}/ModalService.cs (63%) rename src/EventLogExpert.UI/Services/{ => ReleaseNotes}/ReleaseNotesMarkdownRenderer.cs (96%) rename src/EventLogExpert.UI/Services/{ => ReleaseNotes}/ReleaseNotesNormalizer.cs (53%) rename src/EventLogExpert.UI/{Interfaces => Services/Settings}/ISettingsService.cs (100%) rename src/EventLogExpert.UI/Services/{ => Settings}/SettingsService.cs (100%) rename src/EventLogExpert.UI/Services/{ => System}/AppTitleService.cs (89%) create mode 100644 src/EventLogExpert.UI/Services/System/IAppTitleService.cs rename src/EventLogExpert.UI/{Interfaces => Services/System}/IApplicationRestartService.cs (100%) create mode 100644 src/EventLogExpert.UI/Services/System/IMainThreadService.cs rename src/EventLogExpert.UI/{Interfaces => Services/System}/ITitleProvider.cs (100%) rename src/EventLogExpert.UI/{Interfaces => Services/System}/IWindowsIdentityProvider.cs (100%) rename src/EventLogExpert.UI/Services/{ => Update}/GitHubService.cs (94%) create mode 100644 src/EventLogExpert.UI/Services/Update/IGitHubService.cs create mode 100644 src/EventLogExpert.UI/Services/Update/IUpdateService.cs create mode 100644 src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs rename src/EventLogExpert.UI/Services/{ => Update}/UpdateService.cs (96%) rename src/EventLogExpert.UI/Services/{ => Versioning}/CurrentVersionProvider.cs (81%) create mode 100644 src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs rename src/EventLogExpert.UI/{Interfaces => Services/Versioning}/IPackageVersionProvider.cs (100%) rename src/EventLogExpert.UI/Services/{ => Versioning}/PackageVersionProvider.cs (75%) create mode 100644 src/EventLogExpert.UI/Store/EventLog/AddEventAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/AddEventBufferedAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/AddEventSuccessAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/CloseAllAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/CloseLogAction.cs rename src/EventLogExpert.UI/Store/EventLog/{EventLogEffects.cs => Effects.cs} (83%) delete mode 100644 src/EventLogExpert.UI/Store/EventLog/EventLogAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/ILogWatcherService.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/LoadEventsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/LoadEventsPartialAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/LoadNewEventsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/LogReopenInfo.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/LogReopenSnapshot.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/OpenLogAction.cs rename src/EventLogExpert.UI/Store/EventLog/{EventLogReducers.cs => Reducers.cs} (95%) create mode 100644 src/EventLogExpert.UI/Store/EventLog/SelectEventAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/SelectEventsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/SetContinuouslyUpdateAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/SetFiltersAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventLog/SetSelectedEventsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/AddTableAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/AppendTableEventsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/AppendTableEventsBatchAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/CloseAllAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/CloseLogAction.cs rename src/EventLogExpert.UI/Store/EventTable/{EventTableEffects.cs => Effects.cs} (81%) delete mode 100644 src/EventLogExpert.UI/Store/EventTable/EventTableAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/LoadColumnsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/LoadColumnsCompletedAction.cs rename src/EventLogExpert.UI/Store/EventTable/{EventTableReducers.cs => Reducers.cs} (95%) create mode 100644 src/EventLogExpert.UI/Store/EventTable/ReorderColumnAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/ResetColumnDefaultsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/SetActiveTableAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/SetColumnWidthAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/SetOrderByAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/ToggleColumnAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/ToggleLoadingAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/ToggleSortingAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/UpdateDisplayedEventsAction.cs create mode 100644 src/EventLogExpert.UI/Store/EventTable/UpdateTableAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterCompletedAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterCompletedAction.cs rename src/EventLogExpert.UI/Store/FilterCache/{FilterCacheEffects.cs => Effects.cs} (74%) delete mode 100644 src/EventLogExpert.UI/Store/FilterCache/FilterCacheAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/ImportFavoritesAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/LoadFiltersAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/LoadFiltersCompletedAction.cs rename src/EventLogExpert.UI/Store/FilterCache/{FilterCacheReducers.cs => Reducers.cs} (64%) create mode 100644 src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterCompletedAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/AddGroupAction.cs rename src/EventLogExpert.UI/Store/FilterGroup/{FilterGroupEffects.cs => Effects.cs} (72%) delete mode 100644 src/EventLogExpert.UI/Store/FilterGroup/FilterGroupAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/ImportGroupsAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsSuccessAction.cs rename src/EventLogExpert.UI/Store/FilterGroup/{FilterGroupReducers.cs => Reducers.cs} (91%) create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/RemoveFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/RemoveGroupAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/SetFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/SetGroupAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/ToggleFilterExcludedAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterGroup/ToggleGroupAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/AddFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/ApplyFilterGroupAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/ClearAllFiltersAction.cs rename src/EventLogExpert.UI/Store/FilterPane/{FilterPaneEffects.cs => Effects.cs} (77%) delete mode 100644 src/EventLogExpert.UI/Store/FilterPane/FilterPaneAction.cs rename src/EventLogExpert.UI/Store/FilterPane/{FilterPaneReducers.cs => Reducers.cs} (85%) create mode 100644 src/EventLogExpert.UI/Store/FilterPane/RemoveFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/SaveFilterGroupAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/SetFilterAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeSuccessAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/SetIsLoadingAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/ToggleFilterDateAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/ToggleFilterEnabledAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/ToggleFilterExcludedAction.cs create mode 100644 src/EventLogExpert.UI/Store/FilterPane/ToggleIsEnabledAction.cs create mode 100644 src/EventLogExpert.UI/Store/StatusBar/ClearStatusAction.cs create mode 100644 src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs rename src/EventLogExpert.UI/Store/StatusBar/{StatusBarReducers.cs => Reducers.cs} (82%) create mode 100644 src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs create mode 100644 src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs delete mode 100644 src/EventLogExpert.UI/Store/StatusBar/StatusBarAction.cs rename src/{EventLogExpert.UI => EventLogExpert/Interop}/NativeMethods.cs (67%) create mode 100644 src/EventLogExpert/Interop/RestartFlags.cs create mode 100644 src/EventLogExpert/Services/MauiMainThreadService.cs rename src/{EventLogExpert.UI/Services/ApplicationRestartService.cs => EventLogExpert/Services/WindowsApplicationRestartService.cs} (63%) rename src/{EventLogExpert.UI => EventLogExpert}/Services/WindowsIdentityProvider.cs (78%) rename tests/Unit/EventLogExpert.UI.Tests/{ => Defaults}/DatabaseStatusLabelsTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/{ => Defaults}/DateRangeDefaultsTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/{ => Extensions}/FilterMethodsTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Models/{ => Filter}/FilterDataDraftTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/Models/{ => Filter}/FilterDataTests.cs (97%) rename tests/Unit/EventLogExpert.UI.Tests/Models/{ => Filter}/FilterDraftModelTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Models/{ => Filter}/FilterModelTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Models/{ => Filter}/RequiresXmlTests.cs (77%) rename tests/Unit/EventLogExpert.UI.Tests/Models/{ => Update}/GitReleaseModelTests.cs (82%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Alerts}/EmptyLogAlertFormatterTests.cs (96%) create mode 100644 tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Alerts}/ModalAlertDialogServiceTests.cs (84%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Banner}/BannerServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Banner}/BannerViewSelectorTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Database}/DatabaseServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => DebugLog}/DebugLogEntryParserTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => DebugLog}/DebugLogProjectionTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => DebugLog}/DebugLogServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Deployment}/DeploymentServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Display}/ReversedListViewTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Filter}/FilterCategoryItemsCacheTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Filter}/FilterCompilerTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Filter}/FilterServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Menu}/MenuServiceTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Modal}/ModalServiceTests.cs (66%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => ReleaseNotes}/ReleaseNotesMarkdownRendererTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => ReleaseNotes}/ReleaseNotesNormalizerTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Settings}/SettingsServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => System}/AppTitleServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Update}/GitHubServiceTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/Services/{ => Update}/UpdateServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/{EventLogEffectsTests.cs => EffectsTests.cs} (81%) rename tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/{EventTableEffectsTests.cs => EffectsTests.cs} (85%) rename tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/{FilterCacheEffectsTests.cs => EffectsTests.cs} (79%) rename tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/{FilterGroupEffectsTests.cs => EffectsTests.cs} (90%) rename tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/{FilterPaneEffectsTests.cs => EffectsTests.cs} (80%) diff --git a/src/EventLogExpert.Components/Base/ModalBase.cs b/src/EventLogExpert.Components/Base/ModalBase.cs index f993d03f..3c80fea5 100644 --- a/src/EventLogExpert.Components/Base/ModalBase.cs +++ b/src/EventLogExpert.Components/Base/ModalBase.cs @@ -18,6 +18,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 +68,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 +96,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 +106,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 +133,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..8667d3d4 100644 --- a/src/EventLogExpert.Components/Base/ModalChrome.razor.cs +++ b/src/EventLogExpert.Components/Base/ModalChrome.razor.cs @@ -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..7cbd32db 100644 --- a/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs @@ -10,10 +10,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) { - Dispatcher.Dispatch(new FilterPaneAction.SetFilter(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..44f27be2 100644 --- a/src/EventLogExpert.Components/Filters/Base/EditableFilterRowBase.cs +++ b/src/EventLogExpert.Components/Filters/Base/EditableFilterRowBase.cs @@ -173,9 +173,9 @@ 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) { diff --git a/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs index abe0518f..f37dbdb3 100644 --- a/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterCacheRow.razor.cs @@ -29,15 +29,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)); + 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/FilterGroupRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs index 764564bc..ab45f3a3 100644 --- a/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterGroupRow.razor.cs @@ -17,14 +17,14 @@ 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)); + 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) { diff --git a/src/EventLogExpert.Components/Filters/FilterRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterRow.razor.cs index 22546e61..002b3490 100644 --- a/src/EventLogExpert.Components/Filters/FilterRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterRow.razor.cs @@ -18,17 +18,17 @@ 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)); + 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) 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..8ba0f573 100644 --- a/src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs +++ b/src/EventLogExpert.Components/Inputs/ValueSelect.razor.cs @@ -27,6 +27,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 +55,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 +115,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/Menu/MenuRenderer.razor.cs b/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs index 946b6403..0f1bc015 100644 --- a/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs @@ -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/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..49a15aee 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterCache; @@ -34,7 +35,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 +52,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 +61,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,7 +71,7 @@ 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) { @@ -85,11 +86,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..72af5cc0 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI; +using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterGroup; @@ -9,6 +10,7 @@ using Microsoft.AspNetCore.Components; using System.Text.Json; using IDispatcher = Fluxor.IDispatcher; +using SetFilterAction = EventLogExpert.UI.Store.FilterGroup.SetFilterAction; namespace EventLogExpert.Components.Modals.Filters; @@ -65,11 +67,11 @@ protected override void OnParametersSet() 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) @@ -107,7 +109,7 @@ private void HandlePendingSave(FilterDraftModel draft, FilterModel filter) { _pendingDrafts.Remove(draft); - Dispatcher.Dispatch(new FilterGroupAction.SetFilter(Group.Id, filter)); + Dispatcher.Dispatch(new SetFilterAction(Group.Id, filter)); } private async Task ImportGroup() @@ -116,7 +118,7 @@ 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; } @@ -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,7 +50,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; } @@ -58,7 +59,7 @@ protected override async Task OnImportAsync() 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 FilterGroupModel { 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/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index a5e0c9da..2df9b9fc 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; @@ -16,8 +17,8 @@ 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; @@ -41,14 +42,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 +57,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) if (disposing) { _disposed = true; - _classificationCts?.Cancel(); + _classificationCts?.CancelAsync(); _classificationCts?.Dispose(); _classificationCts = null; DatabaseService.EntriesChanged -= OnDatabaseEntriesChanged; @@ -198,7 +199,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; } @@ -281,11 +282,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.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/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..a6738ed2 --- /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.Interfaces; + +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/ColumnDefaults.cs b/src/EventLogExpert.UI/Defaults/ColumnDefaults.cs similarity index 97% rename from src/EventLogExpert.UI/ColumnDefaults.cs rename to src/EventLogExpert.UI/Defaults/ColumnDefaults.cs index dee55cb1..c2886f30 100644 --- a/src/EventLogExpert.UI/ColumnDefaults.cs +++ b/src/EventLogExpert.UI/Defaults/ColumnDefaults.cs @@ -6,7 +6,7 @@ namespace EventLogExpert.UI; -public static class ColumnDefaults +internal static class ColumnDefaults { public static readonly ImmutableList EnabledColumns = [ diff --git a/src/EventLogExpert.UI/DatabaseStatusLabels.cs b/src/EventLogExpert.UI/Defaults/DatabaseStatusLabels.cs similarity index 100% rename from src/EventLogExpert.UI/DatabaseStatusLabels.cs rename to src/EventLogExpert.UI/Defaults/DatabaseStatusLabels.cs diff --git a/src/EventLogExpert.UI/DateRangeDefaults.cs b/src/EventLogExpert.UI/Defaults/DateRangeDefaults.cs similarity index 98% rename from src/EventLogExpert.UI/DateRangeDefaults.cs rename to src/EventLogExpert.UI/Defaults/DateRangeDefaults.cs index 534679ee..4e514b77 100644 --- a/src/EventLogExpert.UI/DateRangeDefaults.cs +++ b/src/EventLogExpert.UI/Defaults/DateRangeDefaults.cs @@ -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/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/Enums/CacheType.cs b/src/EventLogExpert.UI/Enums/CacheType.cs new file mode 100644 index 00000000..89ddcbf6 --- /dev/null +++ b/src/EventLogExpert.UI/Enums/CacheType.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +public enum CacheType +{ + Favorites, + Recent +} diff --git a/src/EventLogExpert.UI/Enums/ColumnName.cs b/src/EventLogExpert.UI/Enums/ColumnName.cs new file mode 100644 index 00000000..0d6bfbde --- /dev/null +++ b/src/EventLogExpert.UI/Enums/ColumnName.cs @@ -0,0 +1,22 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace EventLogExpert.UI; + +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/Enums/CopyType.cs b/src/EventLogExpert.UI/Enums/CopyType.cs new file mode 100644 index 00000000..455931cb --- /dev/null +++ b/src/EventLogExpert.UI/Enums/CopyType.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +public enum CopyType +{ + Default, + Simple, + Xml, + Full +} diff --git a/src/EventLogExpert.UI/Enums/DatabaseStatus.cs b/src/EventLogExpert.UI/Enums/DatabaseStatus.cs new file mode 100644 index 00000000..e77802cd --- /dev/null +++ b/src/EventLogExpert.UI/Enums/DatabaseStatus.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +public enum DatabaseStatus +{ + NotClassified, + Ready, + UpgradeRequired, + UpgradeFailed, + UnrecognizedSchema, + ObsoleteSchema, + ClassificationFailed +} diff --git a/src/EventLogExpert.UI/Enums/FilterCategory.cs b/src/EventLogExpert.UI/Enums/FilterCategory.cs new file mode 100644 index 00000000..6248e7b1 --- /dev/null +++ b/src/EventLogExpert.UI/Enums/FilterCategory.cs @@ -0,0 +1,21 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace EventLogExpert.UI; + +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/Enums/FilterEvaluator.cs b/src/EventLogExpert.UI/Enums/FilterEvaluator.cs new file mode 100644 index 00000000..03ce5748 --- /dev/null +++ b/src/EventLogExpert.UI/Enums/FilterEvaluator.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Runtime.Serialization; + +namespace EventLogExpert.UI; + +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/Enums/FilterType.cs b/src/EventLogExpert.UI/Enums/FilterType.cs new file mode 100644 index 00000000..5efeb98a --- /dev/null +++ b/src/EventLogExpert.UI/Enums/FilterType.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +public enum FilterType +{ + Basic, + Advanced, + Cached +} diff --git a/src/EventLogExpert.UI/Enums/HighlightColor.cs b/src/EventLogExpert.UI/Enums/HighlightColor.cs new file mode 100644 index 00000000..1b718e0e --- /dev/null +++ b/src/EventLogExpert.UI/Enums/HighlightColor.cs @@ -0,0 +1,36 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +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/Enums/OpenLogStatus.cs b/src/EventLogExpert.UI/Enums/OpenLogStatus.cs new file mode 100644 index 00000000..811a4822 --- /dev/null +++ b/src/EventLogExpert.UI/Enums/OpenLogStatus.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +public enum OpenLogStatus +{ + Opened, + Empty, + Skipped, + Failed +} diff --git a/src/EventLogExpert.UI/Enums/Theme.cs b/src/EventLogExpert.UI/Enums/Theme.cs new file mode 100644 index 00000000..094b250d --- /dev/null +++ b/src/EventLogExpert.UI/Enums/Theme.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI; + +public enum Theme +{ + System, + Light, + Dark +} 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/ExtensionMethods.cs b/src/EventLogExpert.UI/Extensions/DisplayExtensions.cs similarity index 95% rename from src/EventLogExpert.UI/ExtensionMethods.cs rename to src/EventLogExpert.UI/Extensions/DisplayExtensions.cs index 65b52899..4cc7f345 100644 --- a/src/EventLogExpert.UI/ExtensionMethods.cs +++ b/src/EventLogExpert.UI/Extensions/DisplayExtensions.cs @@ -6,7 +6,7 @@ namespace EventLogExpert.UI; -public static class ExtensionMethods +public static class DisplayExtensions { public static string ToFullString(this Enum value) { diff --git a/src/EventLogExpert.UI/FilterMethods.cs b/src/EventLogExpert.UI/Extensions/FilterMethods.cs similarity index 99% rename from src/EventLogExpert.UI/FilterMethods.cs rename to src/EventLogExpert.UI/Extensions/FilterMethods.cs index 23139d35..e3fa3a2a 100644 --- a/src/EventLogExpert.UI/FilterMethods.cs +++ b/src/EventLogExpert.UI/Extensions/FilterMethods.cs @@ -6,7 +6,7 @@ namespace EventLogExpert.UI; -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); diff --git a/src/EventLogExpert.UI/Interfaces/IClipboardService.cs b/src/EventLogExpert.UI/Interfaces/IClipboardService.cs index ebd4a2e4..c9d4fa3b 100644 --- a/src/EventLogExpert.UI/Interfaces/IClipboardService.cs +++ b/src/EventLogExpert.UI/Interfaces/IClipboardService.cs @@ -6,15 +6,15 @@ 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. + /// 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. + /// 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 index eff5f545..0663ebd8 100644 --- a/src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs +++ b/src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs @@ -3,21 +3,6 @@ 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). diff --git a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs index e8c32532..85edf897 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs +++ b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs @@ -16,8 +16,8 @@ public interface IMenuActionService 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/InlineAlertRequest.cs b/src/EventLogExpert.UI/Interfaces/InlineAlertRequest.cs new file mode 100644 index 00000000..4b41a831 --- /dev/null +++ b/src/EventLogExpert.UI/Interfaces/InlineAlertRequest.cs @@ -0,0 +1,16 @@ +// // 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); diff --git a/src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs b/src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs new file mode 100644 index 00000000..3b4d38aa --- /dev/null +++ b/src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs @@ -0,0 +1,7 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Interfaces; + +/// 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/Models/BannerInfoEntry.cs b/src/EventLogExpert.UI/Models/Banner/BannerInfoEntry.cs similarity index 100% rename from src/EventLogExpert.UI/Models/BannerInfoEntry.cs rename to src/EventLogExpert.UI/Models/Banner/BannerInfoEntry.cs diff --git a/src/EventLogExpert.UI/Models/BannerProgressEntry.cs b/src/EventLogExpert.UI/Models/Banner/BannerProgressEntry.cs similarity index 100% rename from src/EventLogExpert.UI/Models/BannerProgressEntry.cs rename to src/EventLogExpert.UI/Models/Banner/BannerProgressEntry.cs diff --git a/src/EventLogExpert.UI/Models/BannerSeverity.cs b/src/EventLogExpert.UI/Models/Banner/BannerSeverity.cs similarity index 100% rename from src/EventLogExpert.UI/Models/BannerSeverity.cs rename to src/EventLogExpert.UI/Models/Banner/BannerSeverity.cs diff --git a/src/EventLogExpert.UI/Models/ErrorBannerEntry.cs b/src/EventLogExpert.UI/Models/Banner/ErrorBannerEntry.cs similarity index 100% rename from src/EventLogExpert.UI/Models/ErrorBannerEntry.cs rename to src/EventLogExpert.UI/Models/Banner/ErrorBannerEntry.cs diff --git a/src/EventLogExpert.UI/Models/DatabaseEntry.cs b/src/EventLogExpert.UI/Models/Database/DatabaseEntry.cs similarity index 100% rename from src/EventLogExpert.UI/Models/DatabaseEntry.cs rename to src/EventLogExpert.UI/Models/Database/DatabaseEntry.cs diff --git a/src/EventLogExpert.UI/Models/ImportFailure.cs b/src/EventLogExpert.UI/Models/Database/ImportFailure.cs similarity index 100% rename from src/EventLogExpert.UI/Models/ImportFailure.cs rename to src/EventLogExpert.UI/Models/Database/ImportFailure.cs diff --git a/src/EventLogExpert.UI/Models/ImportResult.cs b/src/EventLogExpert.UI/Models/Database/ImportResult.cs similarity index 100% rename from src/EventLogExpert.UI/Models/ImportResult.cs rename to src/EventLogExpert.UI/Models/Database/ImportResult.cs diff --git a/src/EventLogExpert.UI/Models/DebugLogEntry.cs b/src/EventLogExpert.UI/Models/DebugLog/DebugLogEntry.cs similarity index 100% rename from src/EventLogExpert.UI/Models/DebugLogEntry.cs rename to src/EventLogExpert.UI/Models/DebugLog/DebugLogEntry.cs diff --git a/src/EventLogExpert.UI/Models/EventLogData.cs b/src/EventLogExpert.UI/Models/EventLog/EventLogData.cs similarity index 100% rename from src/EventLogExpert.UI/Models/EventLogData.cs rename to src/EventLogExpert.UI/Models/EventLog/EventLogData.cs diff --git a/src/EventLogExpert.UI/Models/EventLogId.cs b/src/EventLogExpert.UI/Models/EventLog/EventLogId.cs similarity index 100% rename from src/EventLogExpert.UI/Models/EventLogId.cs rename to src/EventLogExpert.UI/Models/EventLog/EventLogId.cs diff --git a/src/EventLogExpert.UI/Models/EventTableModel.cs b/src/EventLogExpert.UI/Models/EventLog/EventTableModel.cs similarity index 100% rename from src/EventLogExpert.UI/Models/EventTableModel.cs rename to src/EventLogExpert.UI/Models/EventLog/EventTableModel.cs diff --git a/src/EventLogExpert.UI/Models/BasicFilter.cs b/src/EventLogExpert.UI/Models/Filter/BasicFilter.cs similarity index 100% rename from src/EventLogExpert.UI/Models/BasicFilter.cs rename to src/EventLogExpert.UI/Models/Filter/BasicFilter.cs diff --git a/src/EventLogExpert.UI/Models/CompiledFilter.cs b/src/EventLogExpert.UI/Models/Filter/CompiledFilter.cs similarity index 100% rename from src/EventLogExpert.UI/Models/CompiledFilter.cs rename to src/EventLogExpert.UI/Models/Filter/CompiledFilter.cs diff --git a/src/EventLogExpert.UI/Models/EventFilter.cs b/src/EventLogExpert.UI/Models/Filter/EventFilter.cs similarity index 90% rename from src/EventLogExpert.UI/Models/EventFilter.cs rename to src/EventLogExpert.UI/Models/Filter/EventFilter.cs index 18534d5f..22cb74cb 100644 --- a/src/EventLogExpert.UI/Models/EventFilter.cs +++ b/src/EventLogExpert.UI/Models/Filter/EventFilter.cs @@ -25,7 +25,10 @@ public EventFilter(FilterDateModel? dateFilter, ImmutableList filte /// 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) diff --git a/src/EventLogExpert.UI/Models/FilterData.cs b/src/EventLogExpert.UI/Models/Filter/FilterData.cs similarity index 73% rename from src/EventLogExpert.UI/Models/FilterData.cs rename to src/EventLogExpert.UI/Models/Filter/FilterData.cs index 6c30bc70..1e45556e 100644 --- a/src/EventLogExpert.UI/Models/FilterData.cs +++ b/src/EventLogExpert.UI/Models/Filter/FilterData.cs @@ -5,9 +5,7 @@ namespace EventLogExpert.UI.Models; -/// -/// 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/Models/Filter/FilterDataDraft.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterDataDraft.cs rename to src/EventLogExpert.UI/Models/Filter/FilterDataDraft.cs diff --git a/src/EventLogExpert.UI/Models/FilterDateModel.cs b/src/EventLogExpert.UI/Models/Filter/FilterDateModel.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterDateModel.cs rename to src/EventLogExpert.UI/Models/Filter/FilterDateModel.cs diff --git a/src/EventLogExpert.UI/Models/FilterDraftModel.cs b/src/EventLogExpert.UI/Models/Filter/FilterDraftModel.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterDraftModel.cs rename to src/EventLogExpert.UI/Models/Filter/FilterDraftModel.cs diff --git a/src/EventLogExpert.UI/Models/FilterGroupData.cs b/src/EventLogExpert.UI/Models/Filter/FilterGroupData.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterGroupData.cs rename to src/EventLogExpert.UI/Models/Filter/FilterGroupData.cs diff --git a/src/EventLogExpert.UI/Models/FilterGroupId.cs b/src/EventLogExpert.UI/Models/Filter/FilterGroupId.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterGroupId.cs rename to src/EventLogExpert.UI/Models/Filter/FilterGroupId.cs diff --git a/src/EventLogExpert.UI/Models/FilterGroupModel.cs b/src/EventLogExpert.UI/Models/Filter/FilterGroupModel.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterGroupModel.cs rename to src/EventLogExpert.UI/Models/Filter/FilterGroupModel.cs diff --git a/src/EventLogExpert.UI/Models/FilterId.cs b/src/EventLogExpert.UI/Models/Filter/FilterId.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterId.cs rename to src/EventLogExpert.UI/Models/Filter/FilterId.cs diff --git a/src/EventLogExpert.UI/Models/FilterModel.cs b/src/EventLogExpert.UI/Models/Filter/FilterModel.cs similarity index 95% rename from src/EventLogExpert.UI/Models/FilterModel.cs rename to src/EventLogExpert.UI/Models/Filter/FilterModel.cs index 0f4b02e0..ecf345a0 100644 --- a/src/EventLogExpert.UI/Models/FilterModel.cs +++ b/src/EventLogExpert.UI/Models/Filter/FilterModel.cs @@ -33,8 +33,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; } diff --git a/src/EventLogExpert.UI/Models/FilterModelJsonConverter.cs b/src/EventLogExpert.UI/Models/Filter/FilterModelJsonConverter.cs similarity index 100% rename from src/EventLogExpert.UI/Models/FilterModelJsonConverter.cs rename to src/EventLogExpert.UI/Models/Filter/FilterModelJsonConverter.cs diff --git a/src/EventLogExpert.UI/Models/SubFilter.cs b/src/EventLogExpert.UI/Models/Filter/SubFilter.cs similarity index 100% rename from src/EventLogExpert.UI/Models/SubFilter.cs rename to src/EventLogExpert.UI/Models/Filter/SubFilter.cs diff --git a/src/EventLogExpert.UI/Models/SubFilterDraft.cs b/src/EventLogExpert.UI/Models/Filter/SubFilterDraft.cs similarity index 100% rename from src/EventLogExpert.UI/Models/SubFilterDraft.cs rename to src/EventLogExpert.UI/Models/Filter/SubFilterDraft.cs diff --git a/src/EventLogExpert.UI/Models/MenuItem.cs b/src/EventLogExpert.UI/Models/Menu/MenuItem.cs similarity index 86% rename from src/EventLogExpert.UI/Models/MenuItem.cs rename to src/EventLogExpert.UI/Models/Menu/MenuItem.cs index 4b8f47e5..a0e7b338 100644 --- a/src/EventLogExpert.UI/Models/MenuItem.cs +++ b/src/EventLogExpert.UI/Models/Menu/MenuItem.cs @@ -4,9 +4,9 @@ namespace EventLogExpert.UI.Models; /// -/// 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/Models/GitReleaseModel.cs b/src/EventLogExpert.UI/Models/Update/GitReleaseModel.cs similarity index 100% rename from src/EventLogExpert.UI/Models/GitReleaseModel.cs rename to src/EventLogExpert.UI/Models/Update/GitReleaseModel.cs diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchCompletedEventArgs.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchCompletedEventArgs.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradeBatchCompletedEventArgs.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchCompletedEventArgs.cs diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchProgressEventArgs.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchProgressEventArgs.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradeBatchProgressEventArgs.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchProgressEventArgs.cs diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchResult.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchResult.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradeBatchResult.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchResult.cs diff --git a/src/EventLogExpert.UI/Models/UpgradeBatchStartedEventArgs.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchStartedEventArgs.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradeBatchStartedEventArgs.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchStartedEventArgs.cs diff --git a/src/EventLogExpert.UI/Models/UpgradeFailure.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradeFailure.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradeFailure.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradeFailure.cs diff --git a/src/EventLogExpert.UI/Models/UpgradePhase.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradePhase.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradePhase.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradePhase.cs diff --git a/src/EventLogExpert.UI/Models/UpgradeProgressScope.cs b/src/EventLogExpert.UI/Models/Upgrade/UpgradeProgressScope.cs similarity index 100% rename from src/EventLogExpert.UI/Models/UpgradeProgressScope.cs rename to src/EventLogExpert.UI/Models/Upgrade/UpgradeProgressScope.cs diff --git a/src/EventLogExpert.UI/Options/FileLocationOptions.cs b/src/EventLogExpert.UI/Options/FileLocationOptions.cs index 7bc201e2..004cb942 100644 --- a/src/EventLogExpert.UI/Options/FileLocationOptions.cs +++ b/src/EventLogExpert.UI/Options/FileLocationOptions.cs @@ -3,7 +3,7 @@ namespace EventLogExpert.UI.Options; -public class FileLocationOptions(string basePath) +public sealed class FileLocationOptions(string basePath) { private readonly string _basePath = basePath; diff --git a/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs b/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs index bf40cb3c..85c66201 100644 --- a/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs +++ b/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs @@ -4,9 +4,15 @@ namespace EventLogExpert.UI.Options; /// 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/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/Alerts/AlertPresentation.cs b/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs new file mode 100644 index 00000000..c3afa7e0 --- /dev/null +++ b/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs @@ -0,0 +1,32 @@ +// // 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/EmptyLogAlertFormatter.cs b/src/EventLogExpert.UI/Services/Alerts/EmptyLogAlertFormatter.cs similarity index 88% rename from src/EventLogExpert.UI/Services/EmptyLogAlertFormatter.cs rename to src/EventLogExpert.UI/Services/Alerts/EmptyLogAlertFormatter.cs index 73d293f7..f78b5085 100644 --- a/src/EventLogExpert.UI/Services/EmptyLogAlertFormatter.cs +++ b/src/EventLogExpert.UI/Services/Alerts/EmptyLogAlertFormatter.cs @@ -4,8 +4,8 @@ namespace EventLogExpert.UI.Services; /// -/// 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/Services/Alerts/IAlertDialogService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IAlertDialogService.cs rename to src/EventLogExpert.UI/Services/Alerts/IAlertDialogService.cs diff --git a/src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs b/src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs new file mode 100644 index 00000000..329935cd --- /dev/null +++ b/src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs @@ -0,0 +1,30 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Interfaces; + +/// +/// 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/Services/Alerts/InlineAlertHostBroker.cs b/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs new file mode 100644 index 00000000..0782deef --- /dev/null +++ b/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs @@ -0,0 +1,57 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Interfaces; + +namespace EventLogExpert.UI.Services; + +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/Services/ModalAlertDialogService.cs b/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs similarity index 95% rename from src/EventLogExpert.UI/Services/ModalAlertDialogService.cs rename to src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs index f1a8b228..ae0b0997 100644 --- a/src/EventLogExpert.UI/Services/ModalAlertDialogService.cs +++ b/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs @@ -7,15 +7,15 @@ namespace EventLogExpert.UI.Services; public sealed class ModalAlertDialogService( - IModalService modalService, + 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/Services/BannerService.cs b/src/EventLogExpert.UI/Services/Banner/BannerService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/BannerService.cs rename to src/EventLogExpert.UI/Services/Banner/BannerService.cs diff --git a/src/EventLogExpert.UI/Services/BannerViewSelector.cs b/src/EventLogExpert.UI/Services/Banner/BannerViewSelector.cs similarity index 100% rename from src/EventLogExpert.UI/Services/BannerViewSelector.cs rename to src/EventLogExpert.UI/Services/Banner/BannerViewSelector.cs diff --git a/src/EventLogExpert.UI/Interfaces/IBannerService.cs b/src/EventLogExpert.UI/Services/Banner/IBannerService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IBannerService.cs rename to src/EventLogExpert.UI/Services/Banner/IBannerService.cs diff --git a/src/EventLogExpert.UI/Services/DatabaseService.cs b/src/EventLogExpert.UI/Services/Database/DatabaseService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/DatabaseService.cs rename to src/EventLogExpert.UI/Services/Database/DatabaseService.cs diff --git a/src/EventLogExpert.UI/Interfaces/IDatabaseService.cs b/src/EventLogExpert.UI/Services/Database/IDatabaseService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IDatabaseService.cs rename to src/EventLogExpert.UI/Services/Database/IDatabaseService.cs diff --git a/src/EventLogExpert.UI/Services/DebugLogEntryParser.cs b/src/EventLogExpert.UI/Services/DebugLog/DebugLogEntryParser.cs similarity index 100% rename from src/EventLogExpert.UI/Services/DebugLogEntryParser.cs rename to src/EventLogExpert.UI/Services/DebugLog/DebugLogEntryParser.cs diff --git a/src/EventLogExpert.UI/Services/DebugLogProjection.cs b/src/EventLogExpert.UI/Services/DebugLog/DebugLogProjection.cs similarity index 100% rename from src/EventLogExpert.UI/Services/DebugLogProjection.cs rename to src/EventLogExpert.UI/Services/DebugLog/DebugLogProjection.cs diff --git a/src/EventLogExpert.UI/Services/DebugLogService.cs b/src/EventLogExpert.UI/Services/DebugLog/DebugLogService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/DebugLogService.cs rename to src/EventLogExpert.UI/Services/DebugLog/DebugLogService.cs diff --git a/src/EventLogExpert.UI/Interfaces/IFileLogger.cs b/src/EventLogExpert.UI/Services/DebugLog/IFileLogger.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IFileLogger.cs rename to src/EventLogExpert.UI/Services/DebugLog/IFileLogger.cs diff --git a/src/EventLogExpert.UI/Services/DeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs similarity index 99% rename from src/EventLogExpert.UI/Services/DeploymentService.cs rename to src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs index 904a76e3..dd9435e5 100644 --- a/src/EventLogExpert.UI/Services/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs @@ -10,7 +10,7 @@ namespace EventLogExpert.UI.Services; -public class DeploymentService( +public sealed class DeploymentService( ITraceLogger traceLogger, IAppTitleService appTitleService, IMainThreadService mainThreadService, @@ -19,8 +19,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/Services/Deployment/IDeploymentService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IDeploymentService.cs rename to src/EventLogExpert.UI/Services/Deployment/IDeploymentService.cs diff --git a/src/EventLogExpert.UI/Interfaces/IPackageDeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/IPackageDeploymentService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IPackageDeploymentService.cs rename to src/EventLogExpert.UI/Services/Deployment/IPackageDeploymentService.cs diff --git a/src/EventLogExpert.UI/Services/PackageDeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/PackageDeploymentService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/PackageDeploymentService.cs rename to src/EventLogExpert.UI/Services/Deployment/PackageDeploymentService.cs diff --git a/src/EventLogExpert.UI/Models/DisplayConverter.cs b/src/EventLogExpert.UI/Services/Display/DisplayConverter.cs similarity index 94% rename from src/EventLogExpert.UI/Models/DisplayConverter.cs rename to src/EventLogExpert.UI/Services/Display/DisplayConverter.cs index e0b5b0b2..b7b377b2 100644 --- a/src/EventLogExpert.UI/Models/DisplayConverter.cs +++ b/src/EventLogExpert.UI/Services/Display/DisplayConverter.cs @@ -3,7 +3,7 @@ namespace EventLogExpert.UI.Models; -public class DisplayConverter +public sealed class DisplayConverter { public Func? GetFunc { get; set; } diff --git a/src/EventLogExpert.UI/Services/ReversedListView.cs b/src/EventLogExpert.UI/Services/Display/ReversedListView.cs similarity index 100% rename from src/EventLogExpert.UI/Services/ReversedListView.cs rename to src/EventLogExpert.UI/Services/Display/ReversedListView.cs diff --git a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs b/src/EventLogExpert.UI/Services/Filter/FilterCategoryItemsCache.cs similarity index 86% rename from src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs rename to src/EventLogExpert.UI/Services/Filter/FilterCategoryItemsCache.cs index 8d7d1efe..b1ef5cfb 100644 --- a/src/EventLogExpert.UI/Services/FilterCategoryItemsCache.cs +++ b/src/EventLogExpert.UI/Services/Filter/FilterCategoryItemsCache.cs @@ -10,9 +10,9 @@ namespace EventLogExpert.UI.Services; /// -/// 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/Services/Filter/FilterCompiler.cs similarity index 100% rename from src/EventLogExpert.UI/Services/FilterCompiler.cs rename to src/EventLogExpert.UI/Services/Filter/FilterCompiler.cs diff --git a/src/EventLogExpert.UI/Services/FilterService.cs b/src/EventLogExpert.UI/Services/Filter/FilterService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/FilterService.cs rename to src/EventLogExpert.UI/Services/Filter/FilterService.cs diff --git a/src/EventLogExpert.UI/Interfaces/IFilterService.cs b/src/EventLogExpert.UI/Services/Filter/IFilterService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IFilterService.cs rename to src/EventLogExpert.UI/Services/Filter/IFilterService.cs 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/IMenuService.cs b/src/EventLogExpert.UI/Services/Menu/IMenuService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IMenuService.cs rename to src/EventLogExpert.UI/Services/Menu/IMenuService.cs diff --git a/src/EventLogExpert.UI/Services/MenuService.cs b/src/EventLogExpert.UI/Services/Menu/MenuService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/MenuService.cs rename to src/EventLogExpert.UI/Services/Menu/MenuService.cs diff --git a/src/EventLogExpert.UI/Interfaces/IModalService.cs b/src/EventLogExpert.UI/Services/Modal/IModalService.cs similarity index 74% rename from src/EventLogExpert.UI/Interfaces/IModalService.cs rename to src/EventLogExpert.UI/Services/Modal/IModalService.cs index 4e3029be..c3e841c7 100644 --- a/src/EventLogExpert.UI/Interfaces/IModalService.cs +++ b/src/EventLogExpert.UI/Services/Modal/IModalService.cs @@ -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/Services/Modal/ModalService.cs similarity index 63% rename from src/EventLogExpert.UI/Services/ModalService.cs rename to src/EventLogExpert.UI/Services/Modal/ModalService.cs index 8e4ce45b..56222a42 100644 --- a/src/EventLogExpert.UI/Services/ModalService.cs +++ b/src/EventLogExpert.UI/Services/Modal/ModalService.cs @@ -10,15 +10,14 @@ 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 +45,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 +62,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 +72,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/Services/ReleaseNotesMarkdownRenderer.cs b/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs similarity index 96% rename from src/EventLogExpert.UI/Services/ReleaseNotesMarkdownRenderer.cs rename to src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs index d05b0090..5d2e9373 100644 --- a/src/EventLogExpert.UI/Services/ReleaseNotesMarkdownRenderer.cs +++ b/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs @@ -9,9 +9,8 @@ namespace EventLogExpert.UI.Services; /// -/// 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 { diff --git a/src/EventLogExpert.UI/Services/ReleaseNotesNormalizer.cs b/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesNormalizer.cs similarity index 53% rename from src/EventLogExpert.UI/Services/ReleaseNotesNormalizer.cs rename to src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesNormalizer.cs index a98e49ea..dddcc9bc 100644 --- a/src/EventLogExpert.UI/Services/ReleaseNotesNormalizer.cs +++ b/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesNormalizer.cs @@ -6,20 +6,15 @@ namespace EventLogExpert.UI.Services; /// -/// 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 ReleaseNotesNormalizer { public static string Normalize(string? rawBody) { @@ -53,11 +48,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 +60,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/Interfaces/ISettingsService.cs b/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/ISettingsService.cs rename to src/EventLogExpert.UI/Services/Settings/ISettingsService.cs diff --git a/src/EventLogExpert.UI/Services/SettingsService.cs b/src/EventLogExpert.UI/Services/Settings/SettingsService.cs similarity index 100% rename from src/EventLogExpert.UI/Services/SettingsService.cs rename to src/EventLogExpert.UI/Services/Settings/SettingsService.cs diff --git a/src/EventLogExpert.UI/Services/AppTitleService.cs b/src/EventLogExpert.UI/Services/System/AppTitleService.cs similarity index 89% rename from src/EventLogExpert.UI/Services/AppTitleService.cs rename to src/EventLogExpert.UI/Services/System/AppTitleService.cs index d7f99e6c..7c99e0b7 100644 --- a/src/EventLogExpert.UI/Services/AppTitleService.cs +++ b/src/EventLogExpert.UI/Services/System/AppTitleService.cs @@ -6,15 +6,6 @@ namespace EventLogExpert.UI.Services; -public interface IAppTitleService -{ - void SetIsPrerelease(bool isPrerelease); - - void SetLogName(string? logName); - - void SetProgressString(string? progressString); -} - public sealed class AppTitleService( ICurrentVersionProvider versionProvider, ITitleProvider titleProvider) : IAppTitleService diff --git a/src/EventLogExpert.UI/Services/System/IAppTitleService.cs b/src/EventLogExpert.UI/Services/System/IAppTitleService.cs new file mode 100644 index 00000000..3411524a --- /dev/null +++ b/src/EventLogExpert.UI/Services/System/IAppTitleService.cs @@ -0,0 +1,13 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Interfaces; + +public interface IAppTitleService +{ + void SetIsPrerelease(bool isPrerelease); + + void SetLogName(string? logName); + + void SetProgressString(string? progressString); +} diff --git a/src/EventLogExpert.UI/Interfaces/IApplicationRestartService.cs b/src/EventLogExpert.UI/Services/System/IApplicationRestartService.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IApplicationRestartService.cs rename to src/EventLogExpert.UI/Services/System/IApplicationRestartService.cs diff --git a/src/EventLogExpert.UI/Services/System/IMainThreadService.cs b/src/EventLogExpert.UI/Services/System/IMainThreadService.cs new file mode 100644 index 00000000..f4a514bb --- /dev/null +++ b/src/EventLogExpert.UI/Services/System/IMainThreadService.cs @@ -0,0 +1,12 @@ +// // 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); +} diff --git a/src/EventLogExpert.UI/Interfaces/ITitleProvider.cs b/src/EventLogExpert.UI/Services/System/ITitleProvider.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/ITitleProvider.cs rename to src/EventLogExpert.UI/Services/System/ITitleProvider.cs diff --git a/src/EventLogExpert.UI/Interfaces/IWindowsIdentityProvider.cs b/src/EventLogExpert.UI/Services/System/IWindowsIdentityProvider.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IWindowsIdentityProvider.cs rename to src/EventLogExpert.UI/Services/System/IWindowsIdentityProvider.cs diff --git a/src/EventLogExpert.UI/Services/GitHubService.cs b/src/EventLogExpert.UI/Services/Update/GitHubService.cs similarity index 94% rename from src/EventLogExpert.UI/Services/GitHubService.cs rename to src/EventLogExpert.UI/Services/Update/GitHubService.cs index f3981b84..b10ca794 100644 --- a/src/EventLogExpert.UI/Services/GitHubService.cs +++ b/src/EventLogExpert.UI/Services/Update/GitHubService.cs @@ -7,11 +7,6 @@ namespace EventLogExpert.UI.Services; -public interface IGitHubService -{ - Task> GetReleases(); -} - public sealed class GitHubService(HttpClient httpClient, ITraceLogger traceLogger) : IGitHubService { private readonly HttpClient _httpClient = httpClient; diff --git a/src/EventLogExpert.UI/Services/Update/IGitHubService.cs b/src/EventLogExpert.UI/Services/Update/IGitHubService.cs new file mode 100644 index 00000000..4603aa19 --- /dev/null +++ b/src/EventLogExpert.UI/Services/Update/IGitHubService.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Services; + +public interface IGitHubService +{ + Task> GetReleases(); +} diff --git a/src/EventLogExpert.UI/Services/Update/IUpdateService.cs b/src/EventLogExpert.UI/Services/Update/IUpdateService.cs new file mode 100644 index 00000000..42bcce99 --- /dev/null +++ b/src/EventLogExpert.UI/Services/Update/IUpdateService.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Services; + +public interface IUpdateService +{ + Task CheckForUpdates(bool usePreRelease, bool userInitiated = false); + + Task GetReleaseNotes(); +} diff --git a/src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs b/src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs new file mode 100644 index 00000000..139f6943 --- /dev/null +++ b/src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Services; + +public readonly record struct ReleaseNotesContent(string Title, string Markdown); diff --git a/src/EventLogExpert.UI/Services/UpdateService.cs b/src/EventLogExpert.UI/Services/Update/UpdateService.cs similarity index 96% rename from src/EventLogExpert.UI/Services/UpdateService.cs rename to src/EventLogExpert.UI/Services/Update/UpdateService.cs index 38bf9cfd..21c438e9 100644 --- a/src/EventLogExpert.UI/Services/UpdateService.cs +++ b/src/EventLogExpert.UI/Services/Update/UpdateService.cs @@ -7,15 +7,6 @@ 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(); -} - public sealed class UpdateService( ICurrentVersionProvider versionProvider, IAppTitleService appTitleService, diff --git a/src/EventLogExpert.UI/Services/CurrentVersionProvider.cs b/src/EventLogExpert.UI/Services/Versioning/CurrentVersionProvider.cs similarity index 81% rename from src/EventLogExpert.UI/Services/CurrentVersionProvider.cs rename to src/EventLogExpert.UI/Services/Versioning/CurrentVersionProvider.cs index 4c381690..1764f799 100644 --- a/src/EventLogExpert.UI/Services/CurrentVersionProvider.cs +++ b/src/EventLogExpert.UI/Services/Versioning/CurrentVersionProvider.cs @@ -5,17 +5,6 @@ namespace EventLogExpert.UI.Services; -public interface ICurrentVersionProvider -{ - Version CurrentVersion { get; } - - bool IsAdmin { get; } - - bool IsDevBuild { get; } - - bool IsSupportedOS(Version currentVersion); -} - public sealed class CurrentVersionProvider( IPackageVersionProvider packageVersionProvider, IWindowsIdentityProvider identityProvider) : ICurrentVersionProvider diff --git a/src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs b/src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs new file mode 100644 index 00000000..c4ece453 --- /dev/null +++ b/src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs @@ -0,0 +1,15 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Services; + +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/Services/Versioning/IPackageVersionProvider.cs similarity index 100% rename from src/EventLogExpert.UI/Interfaces/IPackageVersionProvider.cs rename to src/EventLogExpert.UI/Services/Versioning/IPackageVersionProvider.cs diff --git a/src/EventLogExpert.UI/Services/PackageVersionProvider.cs b/src/EventLogExpert.UI/Services/Versioning/PackageVersionProvider.cs similarity index 75% rename from src/EventLogExpert.UI/Services/PackageVersionProvider.cs rename to src/EventLogExpert.UI/Services/Versioning/PackageVersionProvider.cs index f7fb1042..35f354b2 100644 --- a/src/EventLogExpert.UI/Services/PackageVersionProvider.cs +++ b/src/EventLogExpert.UI/Services/Versioning/PackageVersionProvider.cs @@ -11,6 +11,6 @@ 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/Store/EventLog/AddEventAction.cs b/src/EventLogExpert.UI/Store/EventLog/AddEventAction.cs new file mode 100644 index 00000000..4d9d74a7 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/AddEventAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record AddEventAction(ResolvedEvent NewEvent); diff --git a/src/EventLogExpert.UI/Store/EventLog/AddEventBufferedAction.cs b/src/EventLogExpert.UI/Store/EventLog/AddEventBufferedAction.cs new file mode 100644 index 00000000..a4fc7f95 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/AddEventBufferedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record AddEventBufferedAction(IReadOnlyList UpdatedBuffer, bool IsFull); diff --git a/src/EventLogExpert.UI/Store/EventLog/AddEventSuccessAction.cs b/src/EventLogExpert.UI/Store/EventLog/AddEventSuccessAction.cs new file mode 100644 index 00000000..f6433810 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/AddEventSuccessAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record AddEventSuccessAction(ImmutableDictionary ActiveLogs); diff --git a/src/EventLogExpert.UI/Store/EventLog/CloseAllAction.cs b/src/EventLogExpert.UI/Store/EventLog/CloseAllAction.cs new file mode 100644 index 00000000..dce046a4 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/CloseAllAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/Store/EventLog/CloseLogAction.cs b/src/EventLogExpert.UI/Store/EventLog/CloseLogAction.cs new file mode 100644 index 00000000..b9558fd2 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/CloseLogAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record CloseLogAction(EventLogId LogId, string LogName); diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs b/src/EventLogExpert.UI/Store/EventLog/Effects.cs similarity index 83% rename from src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs rename to src/EventLogExpert.UI/Store/EventLog/Effects.cs index f2caf310..84717118 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogEffects.cs +++ b/src/EventLogExpert.UI/Store/EventLog/Effects.cs @@ -21,7 +21,7 @@ namespace EventLogExpert.UI.Store.EventLog; -public sealed class EventLogEffects( +public sealed class Effects( IState eventLogState, IFilterService filterService, ITraceLogger logger, @@ -33,11 +33,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 +52,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 +97,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 +109,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 +120,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 +134,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 EventTable.CloseAllAction()); + dispatcher.Dispatch(new StatusBar.CloseAllAction()); _resolverCache.ClearAll(); _xmlResolver.ClearAll(); @@ -152,7 +157,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 +196,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 EventTable.CloseLogAction(action.LogId)); if (_eventLogState.Value.ActiveLogs.IsEmpty) { @@ -211,11 +216,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 +244,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 +268,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 +334,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 +360,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 +371,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 +437,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 +471,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 +487,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 +546,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 +608,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 +655,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 +702,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 +740,7 @@ private void CancelAllLoads() } private async Task LoadLogAsync( - EventLogAction.OpenLog action, + OpenLogAction action, EventLogData logData, IEventResolver eventResolver, IServiceScope serviceScope, @@ -780,7 +784,7 @@ private async Task LoadLogAsync( 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 +797,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 +815,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 +920,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 +939,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 +965,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 +1013,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/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/EventLog/ILogReloadCoordinator.cs b/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs index 36f62105..411891a1 100644 --- a/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs +++ b/src/EventLogExpert.UI/Store/EventLog/ILogReloadCoordinator.cs @@ -1,14 +1,12 @@ // // 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 +/// 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 EventLogEffects so close/open dispatches share the same TCS dictionaries 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 @@ -17,24 +15,3 @@ public interface ILogReloadCoordinator void ReopenAfterDatabaseRemoval(IReadOnlyList snapshot); } - -public sealed record LogReopenInfo(string Name, LogPathType Type); - -/// -/// Mutable container that the coordinator populates as each active log finishes closing. Callers pass an empty -/// snapshot in to and then unconditionally pass -/// to in their finally -/// block — that way logs that closed cleanly always get reopened, even when a later phase (file delete, reservation, -/// etc.) throws before all logs were processed. -/// -public sealed class LogReopenSnapshot -{ - private readonly List _items = []; - - public IReadOnlyList Items => _items; - - internal void Add(LogReopenInfo info) - { - _items.Add(info); - } -} diff --git a/src/EventLogExpert.UI/Store/EventLog/ILogWatcherService.cs b/src/EventLogExpert.UI/Store/EventLog/ILogWatcherService.cs new file mode 100644 index 00000000..e21a2bee --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/ILogWatcherService.cs @@ -0,0 +1,13 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.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/Store/EventLog/LoadEventsAction.cs b/src/EventLogExpert.UI/Store/EventLog/LoadEventsAction.cs new file mode 100644 index 00000000..f33daadd --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/LoadEventsAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record LoadEventsAction(EventLogData LogData, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/Store/EventLog/LoadEventsPartialAction.cs b/src/EventLogExpert.UI/Store/EventLog/LoadEventsPartialAction.cs new file mode 100644 index 00000000..782420f6 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/LoadEventsPartialAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record LoadEventsPartialAction(EventLogData LogData, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/Store/EventLog/LoadNewEventsAction.cs b/src/EventLogExpert.UI/Store/EventLog/LoadNewEventsAction.cs new file mode 100644 index 00000000..d7bd9490 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/LoadNewEventsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventLog; + +internal sealed record LoadNewEventsAction; diff --git a/src/EventLogExpert.UI/Store/EventLog/LogReopenInfo.cs b/src/EventLogExpert.UI/Store/EventLog/LogReopenInfo.cs new file mode 100644 index 00000000..97a0965d --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/LogReopenInfo.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Channels; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record LogReopenInfo(string Name, LogPathType Type); diff --git a/src/EventLogExpert.UI/Store/EventLog/LogReopenSnapshot.cs b/src/EventLogExpert.UI/Store/EventLog/LogReopenSnapshot.cs new file mode 100644 index 00000000..fcf6c4cc --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/LogReopenSnapshot.cs @@ -0,0 +1,23 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventLog; + +/// +/// Mutable container that the coordinator populates as each active log finishes closing. Callers pass an empty +/// snapshot in to and then unconditionally pass +/// to in their finally +/// block — that way logs that closed cleanly always get reopened, even when a later phase (file delete, reservation, +/// etc.) throws before all logs were processed. +/// +public sealed class LogReopenSnapshot +{ + private readonly List _items = []; + + public IReadOnlyList Items => _items; + + internal void Add(LogReopenInfo info) + { + _items.Add(info); + } +} diff --git a/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs b/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs index 82274ae5..4ef9fbb6 100644 --- a/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs +++ b/src/EventLogExpert.UI/Store/EventLog/LogWatcherService.cs @@ -9,15 +9,6 @@ namespace EventLogExpert.UI.Store.EventLog; -public interface ILogWatcherService -{ - void AddLog(string logName, string? bookmark, bool renderXml = false); - - Task RemoveAllAsync(); - - Task RemoveLogAsync(string logName); -} - public sealed class LogWatcherService : ILogWatcherService { private readonly Dictionary _bookmarks = []; @@ -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/Store/EventLog/OpenLogAction.cs b/src/EventLogExpert.UI/Store/EventLog/OpenLogAction.cs new file mode 100644 index 00000000..fce782ad --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/OpenLogAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Channels; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record OpenLogAction(string LogName, LogPathType LogPathType, CancellationToken Token = default); diff --git a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs b/src/EventLogExpert.UI/Store/EventLog/Reducers.cs similarity index 95% rename from src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs rename to src/EventLogExpert.UI/Store/EventLog/Reducers.cs index fa9e7ed4..2252e1c9 100644 --- a/src/EventLogExpert.UI/Store/EventLog/EventLogReducers.cs +++ b/src/EventLogExpert.UI/Store/EventLog/Reducers.cs @@ -9,17 +9,17 @@ namespace EventLogExpert.UI.Store.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/Store/EventLog/SelectEventAction.cs b/src/EventLogExpert.UI/Store/EventLog/SelectEventAction.cs new file mode 100644 index 00000000..dc21920e --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/SelectEventAction.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record SelectEventAction( + ResolvedEvent SelectedEvent, + bool IsMultiSelect = false, + bool ShouldStaySelected = false); diff --git a/src/EventLogExpert.UI/Store/EventLog/SelectEventsAction.cs b/src/EventLogExpert.UI/Store/EventLog/SelectEventsAction.cs new file mode 100644 index 00000000..b4bc4797 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/SelectEventsAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record SelectEventsAction(IReadOnlyCollection SelectedEvents); diff --git a/src/EventLogExpert.UI/Store/EventLog/SetContinuouslyUpdateAction.cs b/src/EventLogExpert.UI/Store/EventLog/SetContinuouslyUpdateAction.cs new file mode 100644 index 00000000..b7dbb439 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/SetContinuouslyUpdateAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record SetContinuouslyUpdateAction(bool ContinuouslyUpdate); diff --git a/src/EventLogExpert.UI/Store/EventLog/SetFiltersAction.cs b/src/EventLogExpert.UI/Store/EventLog/SetFiltersAction.cs new file mode 100644 index 00000000..a175a283 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/SetFiltersAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventLog; + +public sealed record SetFiltersAction(EventFilter EventFilter); diff --git a/src/EventLogExpert.UI/Store/EventLog/SetSelectedEventsAction.cs b/src/EventLogExpert.UI/Store/EventLog/SetSelectedEventsAction.cs new file mode 100644 index 00000000..1e20bde0 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventLog/SetSelectedEventsAction.cs @@ -0,0 +1,24 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; + +namespace EventLogExpert.UI.Store.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/Store/EventTable/AddTableAction.cs b/src/EventLogExpert.UI/Store/EventTable/AddTableAction.cs new file mode 100644 index 00000000..7241e6f4 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/AddTableAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record AddTableAction(EventLogData LogData); diff --git a/src/EventLogExpert.UI/Store/EventTable/AppendTableEventsAction.cs b/src/EventLogExpert.UI/Store/EventTable/AppendTableEventsAction.cs new file mode 100644 index 00000000..40bdc900 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/AppendTableEventsAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record AppendTableEventsAction(EventLogId LogId, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/Store/EventTable/AppendTableEventsBatchAction.cs b/src/EventLogExpert.UI/Store/EventTable/AppendTableEventsBatchAction.cs new file mode 100644 index 00000000..c7c22f5a --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/AppendTableEventsBatchAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record AppendTableEventsBatchAction(IReadOnlyDictionary> EventsByLog); diff --git a/src/EventLogExpert.UI/Store/EventTable/CloseAllAction.cs b/src/EventLogExpert.UI/Store/EventTable/CloseAllAction.cs new file mode 100644 index 00000000..2f642f7c --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/CloseAllAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/Store/EventTable/CloseLogAction.cs b/src/EventLogExpert.UI/Store/EventTable/CloseLogAction.cs new file mode 100644 index 00000000..3570239a --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/CloseLogAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record CloseLogAction(EventLogId LogId); diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableEffects.cs b/src/EventLogExpert.UI/Store/EventTable/Effects.cs similarity index 81% rename from src/EventLogExpert.UI/Store/EventTable/EventTableEffects.cs rename to src/EventLogExpert.UI/Store/EventTable/Effects.cs index 8cad903f..13407a82 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableEffects.cs +++ b/src/EventLogExpert.UI/Store/EventTable/Effects.cs @@ -7,12 +7,12 @@ namespace EventLogExpert.UI.Store.EventTable; -public sealed class EventTableEffects(IPreferencesProvider preferencesProvider, IState eventTableState) +public sealed class Effects(IPreferencesProvider preferencesProvider, IState eventTableState) { private readonly IState _eventTableState = eventTableState; private readonly IPreferencesProvider _preferencesProvider = preferencesProvider; - [EffectMethod(typeof(EventTableAction.LoadColumns))] + [EffectMethod(typeof(LoadColumnsAction))] public Task HandleLoadColumns(IDispatcher dispatcher) { var columns = new Dictionary(); @@ -26,13 +26,13 @@ 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; @@ -40,7 +40,7 @@ public Task HandleReorderColumn(EventTableAction.ReorderColumn action, IDispatch return Task.CompletedTask; } - [EffectMethod(typeof(EventTableAction.ResetColumnDefaults))] + [EffectMethod(typeof(ResetColumnDefaultsAction))] public Task HandleResetColumnDefaults(IDispatcher dispatcher) { var columns = new Dictionary(); @@ -56,13 +56,13 @@ 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); @@ -71,7 +71,7 @@ public Task HandleSetColumnWidth(EventTableAction.SetColumnWidth action, IDispat } [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/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/EventTable/LoadColumnsAction.cs b/src/EventLogExpert.UI/Store/EventTable/LoadColumnsAction.cs new file mode 100644 index 00000000..3ce5d962 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/LoadColumnsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +internal sealed record LoadColumnsAction; diff --git a/src/EventLogExpert.UI/Store/EventTable/LoadColumnsCompletedAction.cs b/src/EventLogExpert.UI/Store/EventTable/LoadColumnsCompletedAction.cs new file mode 100644 index 00000000..3690a057 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/LoadColumnsCompletedAction.cs @@ -0,0 +1,11 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record LoadColumnsCompletedAction( + IDictionary LoadedColumns, + IDictionary ColumnWidths, + ImmutableList ColumnOrder); diff --git a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs b/src/EventLogExpert.UI/Store/EventTable/Reducers.cs similarity index 95% rename from src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs rename to src/EventLogExpert.UI/Store/EventTable/Reducers.cs index 468e8f95..c6b52cc2 100644 --- a/src/EventLogExpert.UI/Store/EventTable/EventTableReducers.cs +++ b/src/EventLogExpert.UI/Store/EventTable/Reducers.cs @@ -9,10 +9,10 @@ namespace EventLogExpert.UI.Store.EventTable; -public sealed class EventTableReducers +public sealed class Reducers { [ReducerMethod] - public static EventTableState ReduceAddTable(EventTableState state, EventTableAction.AddTable action) + public static EventTableState ReduceAddTable(EventTableState state, AddTableAction action) { var newTable = new EventTableModel(action.LogData.Id) { @@ -58,7 +58,7 @@ public static EventTableState ReduceAddTable(EventTableState state, EventTableAc } [ReducerMethod] - public static EventTableState ReduceAppendTableEvents(EventTableState state, EventTableAction.AppendTableEvents action) + public static EventTableState ReduceAppendTableEvents(EventTableState state, AppendTableEventsAction action) { var table = state.EventTables.FirstOrDefault(t => action.LogId == t.Id); @@ -86,7 +86,7 @@ public static EventTableState ReduceAppendTableEvents(EventTableState state, Eve [ReducerMethod] public static EventTableState ReduceAppendTableEventsBatch( EventTableState state, - EventTableAction.AppendTableEventsBatch action) + AppendTableEventsBatchAction action) { if (action.EventsByLog.Count == 0) { return state; } @@ -132,7 +132,7 @@ public static EventTableState ReduceAppendTableEventsBatch( }; } - [ReducerMethod(typeof(EventTableAction.CloseAll))] + [ReducerMethod(typeof(CloseAllAction))] public static EventTableState ReduceCloseAll(EventTableState state) => state with { @@ -143,7 +143,7 @@ state with }; [ReducerMethod] - public static EventTableState ReduceCloseLog(EventTableState state, EventTableAction.CloseLog action) + public static EventTableState ReduceCloseLog(EventTableState state, CloseLogAction action) { var closingTable = state.EventTables.FirstOrDefault(table => table.Id == action.LogId); @@ -198,7 +198,7 @@ public static EventTableState ReduceCloseLog(EventTableState state, EventTableAc [ReducerMethod] public static EventTableState ReduceLoadColumnsCompleted( EventTableState state, - EventTableAction.LoadColumnsCompleted action) => + LoadColumnsCompletedAction action) => state with { Columns = action.LoadedColumns.ToImmutableDictionary(), @@ -207,7 +207,7 @@ state with }; [ReducerMethod] - public static EventTableState ReduceReorderColumn(EventTableState state, EventTableAction.ReorderColumn action) + public static EventTableState ReduceReorderColumn(EventTableState state, ReorderColumnAction action) { var order = state.ColumnOrder; @@ -226,7 +226,7 @@ public static EventTableState ReduceReorderColumn(EventTableState state, EventTa } [ReducerMethod] - public static EventTableState ReduceSetActiveTable(EventTableState state, EventTableAction.SetActiveTable action) + public static EventTableState ReduceSetActiveTable(EventTableState state, SetActiveTableAction action) { var activeTable = state.EventTables.FirstOrDefault(table => table.Id == action.LogId); @@ -236,17 +236,17 @@ public static EventTableState ReduceSetActiveTable(EventTableState state, EventT } [ReducerMethod] - public static EventTableState ReduceSetColumnWidth(EventTableState state, EventTableAction.SetColumnWidth action) => + public static EventTableState ReduceSetColumnWidth(EventTableState 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 EventTableState ReduceSetOrderBy(EventTableState 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 EventTableState ReduceToggleLoading(EventTableState state, ToggleLoadingAction action) { var table = state.EventTables.FirstOrDefault(table => table.Id == action.LogId); @@ -260,14 +260,14 @@ public static EventTableState ReduceToggleLoading(EventTableState state, EventTa }; } - [ReducerMethod(typeof(EventTableAction.ToggleSorting))] + [ReducerMethod(typeof(ToggleSortingAction))] public static EventTableState ReduceToggleSorting(EventTableState state) => SortDisplayEvents(state, state.OrderBy, !state.IsDescending); [ReducerMethod] public static EventTableState ReduceUpdateDisplayedEvents( EventTableState state, - EventTableAction.UpdateDisplayedEvents action) + UpdateDisplayedEventsAction action) { // Skip log ids absent from EventTables: log closed while filter ran. var tablesById = state.EventTables @@ -356,7 +356,7 @@ public static EventTableState ReduceUpdateDisplayedEvents( } [ReducerMethod] - public static EventTableState ReduceUpdateTable(EventTableState state, EventTableAction.UpdateTable action) + public static EventTableState ReduceUpdateTable(EventTableState state, UpdateTableAction action) { var table = state.EventTables.FirstOrDefault(t => action.LogId == t.Id); diff --git a/src/EventLogExpert.UI/Store/EventTable/ReorderColumnAction.cs b/src/EventLogExpert.UI/Store/EventTable/ReorderColumnAction.cs new file mode 100644 index 00000000..3c535bb8 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/ReorderColumnAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record ReorderColumnAction(ColumnName ColumnName, ColumnName TargetColumn, bool InsertAfter); diff --git a/src/EventLogExpert.UI/Store/EventTable/ResetColumnDefaultsAction.cs b/src/EventLogExpert.UI/Store/EventTable/ResetColumnDefaultsAction.cs new file mode 100644 index 00000000..2f62889e --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/ResetColumnDefaultsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +internal sealed record ResetColumnDefaultsAction; diff --git a/src/EventLogExpert.UI/Store/EventTable/SetActiveTableAction.cs b/src/EventLogExpert.UI/Store/EventTable/SetActiveTableAction.cs new file mode 100644 index 00000000..8e055ac1 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/SetActiveTableAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record SetActiveTableAction(EventLogId LogId); diff --git a/src/EventLogExpert.UI/Store/EventTable/SetColumnWidthAction.cs b/src/EventLogExpert.UI/Store/EventTable/SetColumnWidthAction.cs new file mode 100644 index 00000000..bed2feea --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/SetColumnWidthAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record SetColumnWidthAction(ColumnName ColumnName, int Width); diff --git a/src/EventLogExpert.UI/Store/EventTable/SetOrderByAction.cs b/src/EventLogExpert.UI/Store/EventTable/SetOrderByAction.cs new file mode 100644 index 00000000..2b8c34b4 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/SetOrderByAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record SetOrderByAction(ColumnName? OrderBy); diff --git a/src/EventLogExpert.UI/Store/EventTable/ToggleColumnAction.cs b/src/EventLogExpert.UI/Store/EventTable/ToggleColumnAction.cs new file mode 100644 index 00000000..802d4d4a --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/ToggleColumnAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record ToggleColumnAction(ColumnName ColumnName); diff --git a/src/EventLogExpert.UI/Store/EventTable/ToggleLoadingAction.cs b/src/EventLogExpert.UI/Store/EventTable/ToggleLoadingAction.cs new file mode 100644 index 00000000..23d42322 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/ToggleLoadingAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record ToggleLoadingAction(EventLogId LogId); diff --git a/src/EventLogExpert.UI/Store/EventTable/ToggleSortingAction.cs b/src/EventLogExpert.UI/Store/EventTable/ToggleSortingAction.cs new file mode 100644 index 00000000..75e5a64a --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/ToggleSortingAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.EventTable; + +internal sealed record ToggleSortingAction; diff --git a/src/EventLogExpert.UI/Store/EventTable/UpdateDisplayedEventsAction.cs b/src/EventLogExpert.UI/Store/EventTable/UpdateDisplayedEventsAction.cs new file mode 100644 index 00000000..c889f485 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/UpdateDisplayedEventsAction.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record UpdateDisplayedEventsAction( + IReadOnlyDictionary> ActiveLogs); diff --git a/src/EventLogExpert.UI/Store/EventTable/UpdateTableAction.cs b/src/EventLogExpert.UI/Store/EventTable/UpdateTableAction.cs new file mode 100644 index 00000000..ace2e267 --- /dev/null +++ b/src/EventLogExpert.UI/Store/EventTable/UpdateTableAction.cs @@ -0,0 +1,9 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Common.Events; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.EventTable; + +public sealed record UpdateTableAction(EventLogId LogId, IReadOnlyList Events); diff --git a/src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterAction.cs b/src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterAction.cs new file mode 100644 index 00000000..cf57dffb --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record AddFavoriteFilterAction(string Filter); diff --git a/src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterCompletedAction.cs b/src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterCompletedAction.cs new file mode 100644 index 00000000..e3bb83cb --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/AddFavoriteFilterCompletedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record AddFavoriteFilterCompletedAction(ImmutableList Filters); diff --git a/src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterAction.cs b/src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterAction.cs new file mode 100644 index 00000000..95e98ed8 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record AddRecentFilterAction(string Filter); diff --git a/src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterCompletedAction.cs b/src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterCompletedAction.cs new file mode 100644 index 00000000..129dfb2c --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/AddRecentFilterCompletedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record AddRecentFilterCompletedAction(ImmutableQueue Filters); diff --git a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheEffects.cs b/src/EventLogExpert.UI/Store/FilterCache/Effects.cs similarity index 74% rename from src/EventLogExpert.UI/Store/FilterCache/FilterCacheEffects.cs rename to src/EventLogExpert.UI/Store/FilterCache/Effects.cs index e1fc4c78..c749f9ce 100644 --- a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheEffects.cs +++ b/src/EventLogExpert.UI/Store/FilterCache/Effects.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; @@ -7,12 +7,12 @@ namespace EventLogExpert.UI.Store.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/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/FilterCache/ImportFavoritesAction.cs b/src/EventLogExpert.UI/Store/FilterCache/ImportFavoritesAction.cs new file mode 100644 index 00000000..762b7406 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/ImportFavoritesAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record ImportFavoritesAction(List Filters); diff --git a/src/EventLogExpert.UI/Store/FilterCache/LoadFiltersAction.cs b/src/EventLogExpert.UI/Store/FilterCache/LoadFiltersAction.cs new file mode 100644 index 00000000..14709c14 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/LoadFiltersAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record LoadFiltersAction; diff --git a/src/EventLogExpert.UI/Store/FilterCache/LoadFiltersCompletedAction.cs b/src/EventLogExpert.UI/Store/FilterCache/LoadFiltersCompletedAction.cs new file mode 100644 index 00000000..8848f37b --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/LoadFiltersCompletedAction.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record LoadFiltersCompletedAction( + ImmutableList FavoriteFilters, + ImmutableQueue RecentFilters); diff --git a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheReducers.cs b/src/EventLogExpert.UI/Store/FilterCache/Reducers.cs similarity index 64% rename from src/EventLogExpert.UI/Store/FilterCache/FilterCacheReducers.cs rename to src/EventLogExpert.UI/Store/FilterCache/Reducers.cs index 9a5d6638..778f321f 100644 --- a/src/EventLogExpert.UI/Store/FilterCache/FilterCacheReducers.cs +++ b/src/EventLogExpert.UI/Store/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; -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/Store/FilterCache/RemoveFavoriteFilterAction.cs b/src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterAction.cs new file mode 100644 index 00000000..cd9410f3 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record RemoveFavoriteFilterAction(string Filter); diff --git a/src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterCompletedAction.cs b/src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterCompletedAction.cs new file mode 100644 index 00000000..3089f9ef --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterCache/RemoveFavoriteFilterCompletedAction.cs @@ -0,0 +1,10 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using System.Collections.Immutable; + +namespace EventLogExpert.UI.Store.FilterCache; + +public sealed record RemoveFavoriteFilterCompletedAction( + ImmutableList FavoriteFilters, + ImmutableQueue RecentFilters); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/AddGroupAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/AddGroupAction.cs new file mode 100644 index 00000000..560ff193 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/AddGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record AddGroupAction(FilterGroupModel? FilterGroup = null); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupEffects.cs b/src/EventLogExpert.UI/Store/FilterGroup/Effects.cs similarity index 72% rename from src/EventLogExpert.UI/Store/FilterGroup/FilterGroupEffects.cs rename to src/EventLogExpert.UI/Store/FilterGroup/Effects.cs index 0564fb4d..92a05026 100644 --- a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupEffects.cs +++ b/src/EventLogExpert.UI/Store/FilterGroup/Effects.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; @@ -6,11 +6,11 @@ namespace EventLogExpert.UI.Store.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/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/FilterGroup/ImportGroupsAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/ImportGroupsAction.cs new file mode 100644 index 00000000..0dab1210 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/ImportGroupsAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record ImportGroupsAction(IEnumerable Groups); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsAction.cs new file mode 100644 index 00000000..0196246f --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterGroup; + +internal sealed record LoadGroupsAction; diff --git a/src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsSuccessAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsSuccessAction.cs new file mode 100644 index 00000000..b5ed19bb --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/LoadGroupsSuccessAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record LoadGroupsSuccessAction(IEnumerable Groups); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupReducers.cs b/src/EventLogExpert.UI/Store/FilterGroup/Reducers.cs similarity index 91% rename from src/EventLogExpert.UI/Store/FilterGroup/FilterGroupReducers.cs rename to src/EventLogExpert.UI/Store/FilterGroup/Reducers.cs index ab5b4fcb..3ce7349e 100644 --- a/src/EventLogExpert.UI/Store/FilterGroup/FilterGroupReducers.cs +++ b/src/EventLogExpert.UI/Store/FilterGroup/Reducers.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Models; @@ -7,24 +7,24 @@ namespace EventLogExpert.UI.Store.FilterGroup; -public sealed class FilterGroupReducers +public sealed class Reducers { [ReducerMethod] - public static FilterGroupState ReducerAddGroup(FilterGroupState state, FilterGroupAction.AddGroup action) => + public static FilterGroupState ReducerAddGroup(FilterGroupState state, AddGroupAction action) => WithGroups(state, state.Groups.Add(action.FilterGroup ?? new FilterGroupModel())); [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); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/RemoveFilterAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/RemoveFilterAction.cs new file mode 100644 index 00000000..67817b22 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/RemoveFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record RemoveFilterAction(FilterGroupId ParentId, FilterId Id); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/RemoveGroupAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/RemoveGroupAction.cs new file mode 100644 index 00000000..ebaaa895 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/RemoveGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record RemoveGroupAction(FilterGroupId Id); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/SetFilterAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/SetFilterAction.cs new file mode 100644 index 00000000..bf2b38cf --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/SetFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record SetFilterAction(FilterGroupId ParentId, FilterModel Filter); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/SetGroupAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/SetGroupAction.cs new file mode 100644 index 00000000..a69c4a0b --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/SetGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record SetGroupAction(FilterGroupModel FilterGroup); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/ToggleFilterExcludedAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/ToggleFilterExcludedAction.cs new file mode 100644 index 00000000..30525f40 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/ToggleFilterExcludedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record ToggleFilterExcludedAction(FilterGroupId ParentId, FilterId Id); diff --git a/src/EventLogExpert.UI/Store/FilterGroup/ToggleGroupAction.cs b/src/EventLogExpert.UI/Store/FilterGroup/ToggleGroupAction.cs new file mode 100644 index 00000000..b4e72108 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterGroup/ToggleGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterGroup; + +public sealed record ToggleGroupAction(FilterGroupId Id); diff --git a/src/EventLogExpert.UI/Store/FilterPane/AddFilterAction.cs b/src/EventLogExpert.UI/Store/FilterPane/AddFilterAction.cs new file mode 100644 index 00000000..90005813 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/AddFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record AddFilterAction(FilterModel FilterModel); diff --git a/src/EventLogExpert.UI/Store/FilterPane/ApplyFilterGroupAction.cs b/src/EventLogExpert.UI/Store/FilterPane/ApplyFilterGroupAction.cs new file mode 100644 index 00000000..9922952c --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/ApplyFilterGroupAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record ApplyFilterGroupAction(FilterGroupModel FilterGroup); diff --git a/src/EventLogExpert.UI/Store/FilterPane/ClearAllFiltersAction.cs b/src/EventLogExpert.UI/Store/FilterPane/ClearAllFiltersAction.cs new file mode 100644 index 00000000..fa6c23ae --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/ClearAllFiltersAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterPane; + +internal sealed record ClearAllFiltersAction; diff --git a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneEffects.cs b/src/EventLogExpert.UI/Store/FilterPane/Effects.cs similarity index 77% rename from src/EventLogExpert.UI/Store/FilterPane/FilterPaneEffects.cs rename to src/EventLogExpert.UI/Store/FilterPane/Effects.cs index 719b37eb..b3ec9873 100644 --- a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneEffects.cs +++ b/src/EventLogExpert.UI/Store/FilterPane/Effects.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Models; @@ -10,7 +10,7 @@ namespace EventLogExpert.UI.Store.FilterPane; -public sealed class FilterPaneEffects( +public sealed class Effects( IState eventLogState, IState filterPaneState) { @@ -18,7 +18,7 @@ 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)) { @@ -29,27 +29,27 @@ public Task HandleAddFilter(FilterPaneAction.AddFilter action, IDispatcher dispa !string.IsNullOrEmpty(action.FilterModel.ComparisonText)) { dispatcher.Dispatch( - new FilterCacheAction.AddRecentFilter(action.FilterModel.ComparisonText)); + new AddRecentFilterAction(action.FilterModel.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,12 +57,12 @@ 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 AddGroupAction( new FilterGroupModel { Name = action.Name, @@ -77,25 +77,25 @@ .. _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) { - dispatcher.Dispatch(new FilterCacheAction.AddRecentFilter(action.FilterModel.ComparisonText)); + dispatcher.Dispatch(new AddRecentFilterAction(action.FilterModel.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) { - dispatcher.Dispatch(new FilterPaneAction.SetFilterDateRangeSuccess(action.FilterDateModel)); + dispatcher.Dispatch(new SetFilterDateRangeSuccessAction(action.FilterDateModel)); return Task.CompletedTask; } @@ -114,7 +114,7 @@ public Task HandleSetFilterDateRange(FilterPaneAction.SetFilterDateRange action, } dispatcher.Dispatch( - new FilterPaneAction.SetFilterDateRangeSuccess( + new SetFilterDateRangeSuccessAction( new FilterDateModel { After = updatedAfter, @@ -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/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/FilterPaneReducers.cs b/src/EventLogExpert.UI/Store/FilterPane/Reducers.cs similarity index 85% rename from src/EventLogExpert.UI/Store/FilterPane/FilterPaneReducers.cs rename to src/EventLogExpert.UI/Store/FilterPane/Reducers.cs index 613a6e73..5f41389f 100644 --- a/src/EventLogExpert.UI/Store/FilterPane/FilterPaneReducers.cs +++ b/src/EventLogExpert.UI/Store/FilterPane/Reducers.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Models; @@ -6,16 +6,16 @@ namespace EventLogExpert.UI.Store.FilterPane; -public sealed class FilterPaneReducers +public sealed class Reducers { [ReducerMethod] - public static FilterPaneState ReduceAddFilter(FilterPaneState state, FilterPaneAction.AddFilter action) => + public static FilterPaneState ReduceAddFilter(FilterPaneState state, AddFilterAction action) => state with { Filters = state.Filters.Add(action.FilterModel) }; [ReducerMethod] public static FilterPaneState ReduceApplyFilterGroup( FilterPaneState state, - FilterPaneAction.ApplyFilterGroup action) + ApplyFilterGroupAction action) { if (!action.FilterGroup.Filters.Any()) { return state; } @@ -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,7 +52,7 @@ 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); @@ -70,10 +70,14 @@ public static FilterPaneState ReduceSetFilter(FilterPaneState state, FilterPaneA [ReducerMethod] public static FilterPaneState ReduceSetFilterDateRangeSuccess( FilterPaneState state, - FilterPaneAction.SetFilterDateRangeSuccess action) => + SetFilterDateRangeSuccessAction action) => state with { FilteredDateRange = action.FilterDateModel }; - [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,23 +91,19 @@ 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, diff --git a/src/EventLogExpert.UI/Store/FilterPane/RemoveFilterAction.cs b/src/EventLogExpert.UI/Store/FilterPane/RemoveFilterAction.cs new file mode 100644 index 00000000..c56e822f --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/RemoveFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record RemoveFilterAction(FilterId Id); diff --git a/src/EventLogExpert.UI/Store/FilterPane/SaveFilterGroupAction.cs b/src/EventLogExpert.UI/Store/FilterPane/SaveFilterGroupAction.cs new file mode 100644 index 00000000..fa600de0 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/SaveFilterGroupAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record SaveFilterGroupAction(string Name); diff --git a/src/EventLogExpert.UI/Store/FilterPane/SetFilterAction.cs b/src/EventLogExpert.UI/Store/FilterPane/SetFilterAction.cs new file mode 100644 index 00000000..6a2cd503 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/SetFilterAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record SetFilterAction(FilterModel FilterModel); diff --git a/src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeAction.cs b/src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeAction.cs new file mode 100644 index 00000000..4aa6b483 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record SetFilterDateRangeAction(FilterDateModel? FilterDateModel); diff --git a/src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeSuccessAction.cs b/src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeSuccessAction.cs new file mode 100644 index 00000000..a2d00af1 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/SetFilterDateRangeSuccessAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record SetFilterDateRangeSuccessAction(FilterDateModel? FilterDateModel); diff --git a/src/EventLogExpert.UI/Store/FilterPane/SetIsLoadingAction.cs b/src/EventLogExpert.UI/Store/FilterPane/SetIsLoadingAction.cs new file mode 100644 index 00000000..5c7bfa4a --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/SetIsLoadingAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record SetIsLoadingAction(bool IsLoading); diff --git a/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterDateAction.cs b/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterDateAction.cs new file mode 100644 index 00000000..5e87f526 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterDateAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterPane; + +internal sealed record ToggleFilterDateAction; diff --git a/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterEnabledAction.cs b/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterEnabledAction.cs new file mode 100644 index 00000000..6f15d4b3 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterEnabledAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record ToggleFilterEnabledAction(FilterId Id); diff --git a/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterExcludedAction.cs b/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterExcludedAction.cs new file mode 100644 index 00000000..eeb6e6ab --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/ToggleFilterExcludedAction.cs @@ -0,0 +1,8 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Store.FilterPane; + +public sealed record ToggleFilterExcludedAction(FilterId Id); diff --git a/src/EventLogExpert.UI/Store/FilterPane/ToggleIsEnabledAction.cs b/src/EventLogExpert.UI/Store/FilterPane/ToggleIsEnabledAction.cs new file mode 100644 index 00000000..92351d22 --- /dev/null +++ b/src/EventLogExpert.UI/Store/FilterPane/ToggleIsEnabledAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.FilterPane; + +internal sealed record ToggleIsEnabledAction; diff --git a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs index f2905586..e0e4dcdc 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Store/LoggingMiddleware.cs @@ -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/Store/StatusBar/ClearStatusAction.cs b/src/EventLogExpert.UI/Store/StatusBar/ClearStatusAction.cs new file mode 100644 index 00000000..da24c413 --- /dev/null +++ b/src/EventLogExpert.UI/Store/StatusBar/ClearStatusAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.StatusBar; + +public sealed record ClearStatusAction(Guid ActivityId); diff --git a/src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs b/src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs new file mode 100644 index 00000000..734813b1 --- /dev/null +++ b/src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.StatusBar; + +public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/Store/StatusBar/StatusBarReducers.cs b/src/EventLogExpert.UI/Store/StatusBar/Reducers.cs similarity index 82% rename from src/EventLogExpert.UI/Store/StatusBar/StatusBarReducers.cs rename to src/EventLogExpert.UI/Store/StatusBar/Reducers.cs index 04e5063e..f3385480 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/StatusBarReducers.cs +++ b/src/EventLogExpert.UI/Store/StatusBar/Reducers.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using Fluxor; @@ -6,10 +6,10 @@ namespace EventLogExpert.UI.Store.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,7 +34,7 @@ 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( diff --git a/src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs b/src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs new file mode 100644 index 00000000..e60f6b97 --- /dev/null +++ b/src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs @@ -0,0 +1,12 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.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(Guid ActivityId, int Count, int FailedCount); diff --git a/src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs b/src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs new file mode 100644 index 00000000..5dbd7f2a --- /dev/null +++ b/src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs @@ -0,0 +1,6 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Store.StatusBar; + +public sealed record SetResolverStatusAction(string ResolverStatus); 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/App.xaml.cs b/src/EventLogExpert/App.xaml.cs index e811c091..8f5c9fe1 100644 --- a/src/EventLogExpert/App.xaml.cs +++ b/src/EventLogExpert/App.xaml.cs @@ -6,7 +6,6 @@ using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; using Fluxor; using System.Collections.Immutable; diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index 25041eb3..9c3d0147 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -21,9 +21,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/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index 6193e71e..9733468d 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -95,7 +95,7 @@ public void OnColumnReordered(string columnName, string targetColumn, bool inser if (Enum.TryParse(columnName, out var column) && Enum.TryParse(targetColumn, out var target)) { - Dispatcher.Dispatch(new EventTableAction.ReorderColumn(column, target, insertAfter)); + Dispatcher.Dispatch(new ReorderColumnAction(column, target, insertAfter)); } } @@ -104,7 +104,7 @@ public void OnColumnResized(string columnName, int width) { if (Enum.TryParse(columnName, out var column)) { - Dispatcher.Dispatch(new EventTableAction.SetColumnWidth(column, width)); + Dispatcher.Dispatch(new SetColumnWidthAction(column, width)); } } @@ -177,11 +177,11 @@ protected override async Task OnInitializedAsync() SelectedEvent.Select(s => s.SelectedEvent); SelectedEvents.Select(s => s.SelectedEvents); - SubscribeToAction(OnSetActiveTable); - SubscribeToAction(_ => RescrollToSelected()); - SubscribeToAction(_ => RescrollToSelected()); - SubscribeToAction(_ => RescrollToSelected()); - SubscribeToAction(_ => RescrollToSelected()); + SubscribeToAction(OnSetActiveTable); + SubscribeToAction(_ => RescrollToSelected()); + SubscribeToAction(_ => RescrollToSelected()); + SubscribeToAction(_ => RescrollToSelected()); + SubscribeToAction(_ => RescrollToSelected()); _eventTableState = EventTableState.Value; @@ -328,7 +328,7 @@ private void ApplySelectedFilter(ResolvedEvent selectedEvent, FilterCategory cat if (filter is null) { return; } - Dispatcher.Dispatch(new FilterPaneAction.SetFilter(filter)); + Dispatcher.Dispatch(new SetFilterAction(filter)); } private IReadOnlyList BuildRange( @@ -400,7 +400,7 @@ private void DispatchSetSelection(IReadOnlyList events, ResolvedE ordered.AddRange(outOfTable); - Dispatcher.Dispatch(new EventLogAction.SetSelectedEvents(ordered, selected)); + Dispatcher.Dispatch(new SetSelectedEventsAction(ordered, selected)); } private async Task FocusActiveRow() @@ -636,7 +636,7 @@ private bool IsSelectionOutOfSortOrder(IReadOnlyList selection) return false; } - private async void OnSetActiveTable(EventTableAction.SetActiveTable action) + private async void OnSetActiveTable(SetActiveTableAction action) { try { @@ -656,7 +656,7 @@ private void RebuildRowIndexMap() if (ReferenceEquals(displayedEvents, _lastIndexedDisplayedEvents)) { return; } _lastIndexedDisplayedEvents = displayedEvents; - _rowIndexMap = new(displayedEvents.Count, ReferenceEqualityComparer.Instance); + _rowIndexMap = new Dictionary(displayedEvents.Count, ReferenceEqualityComparer.Instance); // New event-list reference means stored ResolvedEvent instances // are stale; clearing prevents memory growth across log reloads. _highlightCache.Clear(); @@ -887,7 +887,7 @@ private IReadOnlyList ShowColumnMenuItems() var capturedColumn = column; items.Add(MenuItem.Item( column.ToFullString(), - () => Dispatcher.Dispatch(new EventTableAction.ToggleColumn(capturedColumn)), + () => Dispatcher.Dispatch(new ToggleColumnAction(capturedColumn)), isChecked: isVisible)); } @@ -899,7 +899,7 @@ private IReadOnlyList ShowColumnMenuItems() var capturedColumn = column; orderItems.Add(MenuItem.Item( column.ToFullString(), - () => Dispatcher.Dispatch(new EventTableAction.SetOrderBy(capturedColumn)), + () => Dispatcher.Dispatch(new SetOrderByAction(capturedColumn)), isChecked: state.OrderBy.Equals(capturedColumn))); } @@ -907,7 +907,7 @@ private IReadOnlyList ShowColumnMenuItems() items.Add(MenuItem.Separator()); items.Add(MenuItem.Item( "Reset Column Defaults", - () => Dispatcher.Dispatch(new EventTableAction.ResetColumnDefaults()))); + () => Dispatcher.Dispatch(new ResetColumnDefaultsAction()))); return items; } @@ -922,10 +922,10 @@ private IReadOnlyList ShowContextMenuItems(ResolvedEvent selectedEvent MenuItem.Item("Copy Selected (Full)", () => ClipboardService.CopySelectedEvent(CopyType.Full)), MenuItem.Separator(), MenuItem.Item("Exclude Events Before", () => - Dispatcher.Dispatch(new FilterPaneAction.SetFilterDateRange( + Dispatcher.Dispatch(new SetFilterDateRangeAction( new FilterDateModel { Before = selectedEvent.TimeCreated }))), MenuItem.Item("Exclude Events After", () => - Dispatcher.Dispatch(new FilterPaneAction.SetFilterDateRange( + Dispatcher.Dispatch(new SetFilterDateRangeAction( new FilterDateModel { After = selectedEvent.TimeCreated }))), MenuItem.Separator(), MenuItem.SubMenu("Include", ShowFilterCategoryItems(selectedEvent, exclude: false)), @@ -950,7 +950,7 @@ private IReadOnlyList ShowFilterCategoryItems(ResolvedEvent selectedEv return items; } - private void ToggleSorting() => Dispatcher.Dispatch(new EventTableAction.ToggleSorting()); + private void ToggleSorting() => Dispatcher.Dispatch(new ToggleSortingAction()); private async Task TryRefreshPageSize() { diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor b/src/EventLogExpert/Components/Sections/FilterPane.razor index 61ebc392..d308a725 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor @@ -1,5 +1,4 @@ -@using EventLogExpert.UI -@inherits FluxorComponent +@inherits FluxorComponent
diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs index cc66c7d2..f23f57ed 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs @@ -29,8 +29,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,19 +36,21 @@ 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); }); @@ -98,7 +98,7 @@ private void AddExclusion() private void ApplyDateFilter() { Dispatcher.Dispatch( - new FilterPaneAction.SetFilterDateRange( + new SetFilterDateRangeAction( new FilterDateModel { After = _model.After?.ConvertTimeZoneToUtc(_currentTimeZone), @@ -133,16 +133,16 @@ private void HandleKeyDown(KeyboardEventArgs e) private void HandlePendingSave(FilterDraftModel draft, FilterModel 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; diff --git a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs index 56ba94ff..25e689c9 100644 --- a/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/SplitLogTabPane.razor.cs @@ -3,11 +3,11 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using Fluxor; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; +using CloseLogAction = EventLogExpert.UI.Store.EventLog.CloseLogAction; using IDispatcher = Fluxor.IDispatcher; namespace EventLogExpert.Components.Sections; @@ -64,7 +64,7 @@ private static string GetTabTooltip(EventTableModel table) } private void CloseLog(EventTableModel table) => - Dispatcher.Dispatch(new EventLogAction.CloseLog(table.Id, table.LogName)); + Dispatcher.Dispatch(new CloseLogAction(table.Id, table.LogName)); private string GetActiveTab(EventTableModel table) => EventTableState.Value.ActiveEventLogId == table.Id ? "tab active" : "tab"; @@ -85,5 +85,5 @@ private string GetTabName(EventTableModel table) } private void SetActiveLog(EventTableModel table) => - Dispatcher.Dispatch(new EventTableAction.SetActiveTable(table.Id)); + Dispatcher.Dispatch(new SetActiveTableAction(table.Id)); } diff --git a/src/EventLogExpert/Components/Sections/StatusBar.razor b/src/EventLogExpert/Components/Sections/StatusBar.razor index e549bd8b..a6b93697 100644 --- a/src/EventLogExpert/Components/Sections/StatusBar.razor +++ b/src/EventLogExpert/Components/Sections/StatusBar.razor @@ -1,5 +1,4 @@ @using EventLogExpert.Eventing.Common.Channels -@using EventLogExpert.UI @inherits FluxorComponent
diff --git a/src/EventLogExpert/EventLogExpert.csproj b/src/EventLogExpert/EventLogExpert.csproj index 9439a263..9e4a44ba 100644 --- a/src/EventLogExpert/EventLogExpert.csproj +++ b/src/EventLogExpert/EventLogExpert.csproj @@ -6,6 +6,7 @@ true true false + true True False diff --git a/src/EventLogExpert.UI/NativeMethods.cs b/src/EventLogExpert/Interop/NativeMethods.cs similarity index 67% rename from src/EventLogExpert.UI/NativeMethods.cs rename to src/EventLogExpert/Interop/NativeMethods.cs index 8fabab8f..bcfe2a85 100644 --- a/src/EventLogExpert.UI/NativeMethods.cs +++ b/src/EventLogExpert/Interop/NativeMethods.cs @@ -7,24 +7,7 @@ // We are defining some win32 types in this file, so we // are not following the usual C# naming conventions. -namespace EventLogExpert.UI; - -/// Flags for the RegisterApplicationRestart function -[Flags] -internal enum RestartFlags -{ - /// None of the options below. - NONE = 0, - - /// Do not restart the process if it terminates due to an unhandled exception. - RESTART_NO_CRASH = 1, - /// Do not restart the process if it terminates due to the application not responding. - RESTART_NO_HANG = 2, - /// Do not restart the process if it terminates due to the installation of an update. - RESTART_NO_PATCH = 4, - /// Do not restart the process if the computer is restarted as the result of an update. - RESTART_NO_REBOOT = 8 -} +namespace EventLogExpert.Interop; internal static partial class NativeMethods { diff --git a/src/EventLogExpert/Interop/RestartFlags.cs b/src/EventLogExpert/Interop/RestartFlags.cs new file mode 100644 index 00000000..eccef9ae --- /dev/null +++ b/src/EventLogExpert/Interop/RestartFlags.cs @@ -0,0 +1,25 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +// ReSharper disable InconsistentNaming +// We are defining some win32 types in this file, so we +// are not following the usual C# naming conventions. + +namespace EventLogExpert.Interop; + +/// Flags for the RegisterApplicationRestart function +[Flags] +internal enum RestartFlags +{ + /// None of the options below. + NONE = 0, + + /// Do not restart the process if it terminates due to an unhandled exception. + RESTART_NO_CRASH = 1, + /// Do not restart the process if it terminates due to the application not responding. + RESTART_NO_HANG = 2, + /// Do not restart the process if it terminates due to the installation of an update. + RESTART_NO_PATCH = 4, + /// Do not restart the process if the computer is restarted as the result of an update. + RESTART_NO_REBOOT = 8 +} diff --git a/src/EventLogExpert/MainPage.xaml.cs b/src/EventLogExpert/MainPage.xaml.cs index 60aa1953..ab4939b1 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.cs @@ -7,7 +7,6 @@ using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.FilterCache; @@ -56,9 +55,9 @@ public MainPage( _activeLogs.SelectedValueChanged += OnActiveLogsChanged; _settings.ThemeChanged += OnThemeChanged; - fluxorDispatcher.Dispatch(new EventTableAction.LoadColumns()); - fluxorDispatcher.Dispatch(new FilterCacheAction.LoadFilters()); - fluxorDispatcher.Dispatch(new FilterGroupAction.LoadGroups()); + fluxorDispatcher.Dispatch(new LoadColumnsAction()); + fluxorDispatcher.Dispatch(new LoadFiltersAction()); + fluxorDispatcher.Dispatch(new LoadGroupsAction()); _ = ProcessCommandLine(); } diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 3f2caf4c..eb9863b9 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Services; +using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; @@ -43,12 +44,12 @@ public static MauiApp CreateMauiApp() options.AddMiddleware(); }); - // EventLogEffects implements ILogReloadCoordinator. Fluxor registers Effects as singletons + // Effects implements ILogReloadCoordinator. Fluxor registers Effects as singletons // by assembly scan; resolve the same instance through the coordinator interface so callers // (SettingsModal) get the single per-app instance with its dictionaries of in-flight loads // and close completions. builder.Services.AddSingleton(sp => - sp.GetRequiredService()); + sp.GetRequiredService()); // Core Services builder.Services.AddSingleton(); @@ -60,7 +61,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(fileLocationOptions); Directory.CreateDirectory(fileLocationOptions.DatabasePath); - builder.Services.AddSingleton(_ => + builder.Services.AddSingleton(_ => { HttpClient client = new() { BaseAddress = new Uri("https://api.github.com/") }; @@ -75,7 +76,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -91,9 +92,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddTransient(); // UI Services - builder.Services.AddSingleton(new MainThreadService( - mainThreadInvoker: action => MainThread.InvokeOnMainThreadAsync(action), - mainThreadAsyncInvoker: taskFactory => MainThread.InvokeOnMainThreadAsync(taskFactory))); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -104,6 +103,7 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(static provider => @@ -115,11 +115,12 @@ public static MauiApp CreateMauiApp() builder.Services.AddSingleton(static provider => { var modalService = provider.GetRequiredService(); + var inlineAlertHostBroker = provider.GetRequiredService(); var mainThreadService = provider.GetRequiredService(); var bannerService = provider.GetRequiredService(); return new ModalAlertDialogService( - modalService, + inlineAlertHostBroker, mainThreadService, bannerService, parameters => modalService.Show(parameters.ToDictionary(static kvp => kvp.Key, static kvp => kvp.Value)), diff --git a/src/EventLogExpert/Platforms/Windows/App.xaml.cs b/src/EventLogExpert/Platforms/Windows/App.xaml.cs index 925869d3..796dcc7d 100644 --- a/src/EventLogExpert/Platforms/Windows/App.xaml.cs +++ b/src/EventLogExpert/Platforms/Windows/App.xaml.cs @@ -6,18 +6,16 @@ namespace EventLogExpert.WinUI; -/// -/// Provides application-specific behavior to supplement the default Application class. -/// +/// Provides application-specific behavior to supplement the default Application class. public partial class App : MauiWinUIApplication { /// - /// Initializes the singleton application object. This is the first line of authored code - /// executed, and as such is the logical equivalent of main() or WinMain(). + /// Initializes the singleton application object. This is the first line of authored code executed, and as such + /// is the logical equivalent of main() or WinMain(). /// public App() { - this.InitializeComponent(); + InitializeComponent(); } protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp(); diff --git a/src/EventLogExpert/Platforms/Windows/FolderPickerHelper.cs b/src/EventLogExpert/Platforms/Windows/FolderPickerHelper.cs index 6edd654a..a2ed1f28 100644 --- a/src/EventLogExpert/Platforms/Windows/FolderPickerHelper.cs +++ b/src/EventLogExpert/Platforms/Windows/FolderPickerHelper.cs @@ -10,9 +10,9 @@ namespace EventLogExpert.Platforms.Windows; public static class FolderPickerHelper { /// - /// Presents the WinUI folder picker. Returns the selected folder's path, or null only when the - /// user cancelled. Throws when no MAUI host window is available - /// so callers can surface the broken-host condition instead of silently treating it as a cancel. + /// Presents the WinUI folder picker. Returns the selected folder's path, or null only when the user + /// cancelled. Throws when no MAUI host window is available so callers can + /// surface the broken-host condition instead of silently treating it as a cancel. /// public static async Task PickFolderAsync() { diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index fae0e770..99938905 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -11,6 +11,7 @@ using Fluxor; using System.Collections.Immutable; using System.Text; +using System.Xml; using System.Xml.Linq; namespace EventLogExpert.Services; @@ -81,7 +82,7 @@ private static string FormatXmlForCopy(string xml) { return XElement.Parse(xml).ToString(); } - catch (System.Xml.XmlException) + catch (XmlException) { return xml; } diff --git a/src/EventLogExpert/Services/KeyboardShortcutService.cs b/src/EventLogExpert/Services/KeyboardShortcutService.cs index 6960081f..78d2718e 100644 --- a/src/EventLogExpert/Services/KeyboardShortcutService.cs +++ b/src/EventLogExpert/Services/KeyboardShortcutService.cs @@ -8,8 +8,8 @@ namespace EventLogExpert.Services; /// /// Hosts the .NET side of the JS keydown bridge. Webview JS calls for any -/// Ctrl-modified key it considers menu-relevant after the bridge has already synchronously suppressed the -/// browser default; this class then decides whether to run the corresponding action. +/// Ctrl-modified key it considers menu-relevant after the bridge has already synchronously suppressed the browser +/// default; this class then decides whether to run the corresponding action. /// public sealed class KeyboardShortcutService( IMenuActionService actions, @@ -27,25 +27,6 @@ public sealed class KeyboardShortcutService( public async ValueTask DisposeAsync() => await UnregisterAsync(); - public async ValueTask UnregisterAsync() - { - if (_selfRef is null) { return; } - - if (_jsRuntime is not null) - { - try - { - await _jsRuntime.InvokeVoidAsync("unregisterKeyboardShortcuts"); - } - catch (JSDisconnectedException) { /* Circuit gone — listener already detached. */ } - catch (TaskCanceledException) { /* Teardown cancellation; nothing to do. */ } - } - - _selfRef.Dispose(); - _selfRef = null; - _registered = false; - } - public async Task EnsureRegisteredAsync(IJSRuntime jsRuntime) { // The JS bridge is idempotent on re-register and refreshes its DotNetObjectReference, so we @@ -80,11 +61,11 @@ public async Task EnsureRegisteredAsync(IJSRuntime jsRuntime) } /// - /// Runs the requested shortcut action when applicable. The JS bridge has already synchronously - /// called preventDefault/stopPropagation in capture phase before invoking this method, - /// so the return value would be ignored — this method is intentionally rather - /// than Task<bool>. When a modal is active, the action is skipped (no-op) so modal - /// keybindings stay isolated; the browser default has still been suppressed by the bridge. + /// Runs the requested shortcut action when applicable. The JS bridge has already synchronously called + /// preventDefault/stopPropagation in capture phase before invoking this method, so the return value + /// would be ignored — this method is intentionally rather than Task<bool>. When a + /// modal is active, the action is skipped (no-op) so modal keybindings stay isolated; the browser default has still + /// been suppressed by the bridge. /// [JSInvokable] public async Task HandleShortcutAsync(string code, bool ctrl, bool alt, bool shift, bool meta) @@ -109,4 +90,23 @@ public async Task HandleShortcutAsync(string code, bool ctrl, bool alt, bool shi return; } } + + public async ValueTask UnregisterAsync() + { + if (_selfRef is null) { return; } + + if (_jsRuntime is not null) + { + try + { + await _jsRuntime.InvokeVoidAsync("unregisterKeyboardShortcuts"); + } + catch (JSDisconnectedException) { /* Circuit gone — listener already detached. */ } + catch (TaskCanceledException) { /* Teardown cancellation; nothing to do. */ } + } + + _selfRef.Dispose(); + _selfRef = null; + _registered = false; + } } diff --git a/src/EventLogExpert/Services/MauiFilePickerService.cs b/src/EventLogExpert/Services/MauiFilePickerService.cs index ca8e21e5..b49c1e5f 100644 --- a/src/EventLogExpert/Services/MauiFilePickerService.cs +++ b/src/EventLogExpert/Services/MauiFilePickerService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Files; namespace EventLogExpert.Services; diff --git a/src/EventLogExpert/Services/MauiFileSaveService.cs b/src/EventLogExpert/Services/MauiFileSaveService.cs index 0e95bdb8..c795bf7e 100644 --- a/src/EventLogExpert/Services/MauiFileSaveService.cs +++ b/src/EventLogExpert/Services/MauiFileSaveService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Files; using Windows.Storage; using Windows.Storage.Pickers; using Windows.Storage.Provider; diff --git a/src/EventLogExpert/Services/MauiMainThreadService.cs b/src/EventLogExpert/Services/MauiMainThreadService.cs new file mode 100644 index 00000000..c82e35a4 --- /dev/null +++ b/src/EventLogExpert/Services/MauiMainThreadService.cs @@ -0,0 +1,24 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Services; + +namespace EventLogExpert.Services; + +internal sealed class MauiMainThreadService : IMainThreadService +{ + public Task InvokeOnMainThread(Action action) => + InvokeSafely(() => MainThread.InvokeOnMainThreadAsync(action)); + + public Task InvokeOnMainThreadAsync(Func action) => + InvokeSafely(() => MainThread.InvokeOnMainThreadAsync(action)); + + // Captures synchronous throws from MainThread.InvokeOnMainThreadAsync into a faulted Task so + // consumers wiring .ContinueWith(OnlyOnFaulted) on the returned Task (e.g. DeploymentService + // WinRT callbacks) observe failures rather than swallowing an unobserved synchronous throw. + private static Task InvokeSafely(Func dispatch) + { + try { return dispatch(); } + catch (Exception ex) { return Task.FromException(ex); } + } +} diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 8662b271..6922fdea 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -22,8 +22,8 @@ namespace EventLogExpert.Services; /// /// MAUI implementation of . Owns the cancellation token that gates background /// log loads (replaces the legacy field on MainPage). Exposes publicly so the -/// page-level drag/drop and command-line handlers can batch multiple opens through the same cancellation lifecycle -/// and surface one banner alert per gesture for empty logs. +/// page-level drag/drop and command-line handlers can batch multiple opens through the same cancellation lifecycle and +/// surface one banner alert per gesture for empty logs. /// public sealed class MauiMenuActionService( IDispatcher dispatcher, @@ -63,12 +63,12 @@ public async Task CheckForUpdatesAsync() await _updateService.CheckForUpdates(_settings.IsPreReleaseEnabled, userInitiated: true); } - public void ClearAllFilters() => _dispatcher.Dispatch(new FilterPaneAction.ClearAllFilters()); + public void ClearAllFilters() => _dispatcher.Dispatch(new ClearAllFiltersAction()); public async Task CloseAllLogsAsync() { await _cancellationTokenSource.CancelAsync(); - _dispatcher.Dispatch(new EventLogAction.CloseAll()); + _dispatcher.Dispatch(new CloseAllAction()); } public async Task CopySelectedAsync(CopyType? copyType) => await _clipboardService.CopySelectedEvent(copyType); @@ -129,7 +129,7 @@ public async Task> GetOtherLogNamesAsync() } } - public void LoadNewEvents() => _dispatcher.Dispatch(new EventLogAction.LoadNewEvents()); + public void LoadNewEvents() => _dispatcher.Dispatch(new LoadNewEventsAction()); public Task OpenDocsAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/blob/main/docs/Home.md"); @@ -231,7 +231,7 @@ await _dialogService.ShowAlert( if (!combineLog) { await _cancellationTokenSource.CancelAsync(); - _dispatcher.Dispatch(new EventLogAction.CloseAll()); + _dispatcher.Dispatch(new CloseAllAction()); } if (_cancellationTokenSource.IsCancellationRequested) @@ -239,16 +239,16 @@ await _dialogService.ShowAlert( _cancellationTokenSource = new CancellationTokenSource(); } - _dispatcher.Dispatch(new EventLogAction.OpenLog(logPath, pathType, _cancellationTokenSource.Token)); + _dispatcher.Dispatch(new OpenLogAction(logPath, pathType, _cancellationTokenSource.Token)); return OpenLogStatus.Opened; } /// /// Opens each log in sequentially and surfaces a single banner alert at the end naming - /// every log that contained zero events. controls whether the first successful - /// open closes existing logs first; subsequent opens within the batch always coalesce with the new state. Use - /// this from any call site that may open multiple logs in one user gesture (multi-file picker, folder open, - /// drag-drop, command line) so the user sees one batched alert instead of one popup per empty file. + /// every log that contained zero events. controls whether the first successful open + /// closes existing logs first; subsequent opens within the batch always coalesce with the new state. Use this from any + /// call site that may open multiple logs in one user gesture (multi-file picker, folder open, drag-drop, command line) + /// so the user sees one batched alert instead of one popup per empty file. /// public async Task OpenLogsBatchAsync(IEnumerable<(string Path, LogPathType Type)> logs, bool combineLog) { @@ -293,11 +293,11 @@ public async Task SaveAllFiltersAsync() if (string.IsNullOrEmpty(groupName)) { return; } - _dispatcher.Dispatch(new FilterPaneAction.SaveFilterGroup(groupName)); + _dispatcher.Dispatch(new SaveFilterGroupAction(groupName)); } public void SetContinuouslyUpdate(bool value) => - _dispatcher.Dispatch(new EventLogAction.SetContinuouslyUpdate(value)); + _dispatcher.Dispatch(new SetContinuouslyUpdateAction(value)); public Task ShowDebugLogsAsync() => ShowModalAsync("debug logs"); @@ -322,7 +322,7 @@ await _modalService.Show( } } - public void ToggleShowAllEvents() => _dispatcher.Dispatch(new FilterPaneAction.ToggleIsEnabled()); + public void ToggleShowAllEvents() => _dispatcher.Dispatch(new ToggleIsEnabledAction()); private static string GetEmptyLogDisplayName(string path, LogPathType type) { diff --git a/src/EventLogExpert.UI/Services/ApplicationRestartService.cs b/src/EventLogExpert/Services/WindowsApplicationRestartService.cs similarity index 63% rename from src/EventLogExpert.UI/Services/ApplicationRestartService.cs rename to src/EventLogExpert/Services/WindowsApplicationRestartService.cs index a1dc942a..82e7623a 100644 --- a/src/EventLogExpert.UI/Services/ApplicationRestartService.cs +++ b/src/EventLogExpert/Services/WindowsApplicationRestartService.cs @@ -2,12 +2,13 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.Interop; using EventLogExpert.UI.Interfaces; using Windows.ApplicationModel.Core; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.Services; -public sealed class ApplicationRestartService(ITraceLogger traceLogger) : IApplicationRestartService +internal sealed class WindowsApplicationRestartService(ITraceLogger traceLogger) : IApplicationRestartService { private readonly ITraceLogger _traceLogger = traceLogger; @@ -26,16 +27,16 @@ public async Task TryRestartAsync(string launchArguments = "") if (reason == AppRestartFailureReason.RestartPending) { - _traceLogger.Information($"{nameof(ApplicationRestartService)}.{nameof(TryRestartAsync)}: restart already pending"); + _traceLogger.Information($"{nameof(WindowsApplicationRestartService)}.{nameof(TryRestartAsync)}: restart already pending"); return true; } - _traceLogger.Error($"{nameof(ApplicationRestartService)}.{nameof(TryRestartAsync)}: restart denied: {reason}"); + _traceLogger.Error($"{nameof(WindowsApplicationRestartService)}.{nameof(TryRestartAsync)}: restart denied: {reason}"); return false; } catch (Exception ex) { - _traceLogger.Error($"{nameof(ApplicationRestartService)}.{nameof(TryRestartAsync)}: restart threw: {ex}"); + _traceLogger.Error($"{nameof(WindowsApplicationRestartService)}.{nameof(TryRestartAsync)}: restart threw: {ex}"); return false; } } diff --git a/src/EventLogExpert.UI/Services/WindowsIdentityProvider.cs b/src/EventLogExpert/Services/WindowsIdentityProvider.cs similarity index 78% rename from src/EventLogExpert.UI/Services/WindowsIdentityProvider.cs rename to src/EventLogExpert/Services/WindowsIdentityProvider.cs index 422f04bd..f97091e1 100644 --- a/src/EventLogExpert.UI/Services/WindowsIdentityProvider.cs +++ b/src/EventLogExpert/Services/WindowsIdentityProvider.cs @@ -4,9 +4,9 @@ using EventLogExpert.UI.Interfaces; using System.Security.Principal; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.Services; -public sealed class WindowsIdentityProvider : IWindowsIdentityProvider +internal sealed class WindowsIdentityProvider : IWindowsIdentityProvider { public bool IsUserInAdministratorRole() { diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs index c802f448..b04ace3e 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/GlobalUsings.cs @@ -1 +1,4 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + global using Xunit; diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs index 625baa53..cecf5a3f 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/Readers/SmallEvtxFixture.cs @@ -8,7 +8,10 @@ namespace EventLogExpert.Eventing.IntegrationTests.Readers; -/// Creates a small temporary .evtx file by exporting a bounded window of events from the live local Application log, anchored on the most recent EventRecordID. +/// +/// Creates a small temporary .evtx file by exporting a bounded window of events from the live local Application +/// log, anchored on the most recent EventRecordID. +/// internal sealed class SmallEvtxFixture : IDisposable { private const int MinimumExpectedEvents = 2; diff --git a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs index e6421df2..91b986c1 100644 --- a/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs +++ b/tests/Integration/EventLogExpert.Eventing.IntegrationTests/TestUtils/Constants/Constants.Provider.cs @@ -6,13 +6,13 @@ namespace EventLogExpert.Eventing.IntegrationTests.TestUtils.Constants; public sealed partial class Constants { public const string ApplicationLogName = "Application"; - public const string SystemLogName = "System"; - public const string SecurityLogName = "Security"; public const string KernelGeneralLogName = "Microsoft-Windows-Kernel-General"; public const string PowerShellLogName = "Microsoft-Windows-PowerShell"; public const string SecurityAuditingLogName = "Microsoft-Windows-Security-Auditing"; + public const string SecurityLogName = "Security"; public const string ServiceControlManagerLogName = "Service Control Manager"; + public const string SystemLogName = "System"; public const string TestProviderName = "TestProvider"; } diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs index a2026c1a..09a59879 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -6,7 +6,9 @@ using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; +using Microsoft.JSInterop; using NSubstitute; namespace EventLogExpert.Components.Tests; @@ -44,7 +46,7 @@ public async Task BannerHost_AttentionDismissClicked_CallsDismissAttention() _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]); var component = Render(); - await component.Find("aside.banner-attention button.banner-dismiss").ClickAsync(new()); + await component.Find("aside.banner-attention button.banner-dismiss").ClickAsync(new MouseEventArgs()); _bannerService.Received(1).DismissAttention(); } @@ -61,29 +63,29 @@ public void BannerHost_AttentionDismissed_DoesNotRenderAttentionBanner() } [Fact] - public void BannerHost_AttentionEntries_RendersAttentionBannerWithOpenSettingsAndDismiss() + public void BannerHost_AttentionEntriesSingleEntry_UsesSingularDatabaseLabel() { - _bannerService.AttentionEntries.Returns( - [BuildDatabaseEntry("a.db"), BuildDatabaseEntry("b.db")]); + _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]); var component = Render(); var banner = component.Find("aside.banner-attention"); - Assert.Contains("2 databases need attention", banner.TextContent); - Assert.Equal("Open Settings", component.Find("aside.banner-attention button.banner-action").TextContent.Trim()); - Assert.Single(component.FindAll("aside.banner-attention button.banner-dismiss")); + Assert.Contains("1 database need", banner.TextContent); + Assert.DoesNotContain("databases need", banner.TextContent); } [Fact] - public void BannerHost_AttentionEntriesSingleEntry_UsesSingularDatabaseLabel() + public void BannerHost_AttentionEntries_RendersAttentionBannerWithOpenSettingsAndDismiss() { - _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]); + _bannerService.AttentionEntries.Returns( + [BuildDatabaseEntry("a.db"), BuildDatabaseEntry("b.db")]); var component = Render(); var banner = component.Find("aside.banner-attention"); - Assert.Contains("1 database need", banner.TextContent); - Assert.DoesNotContain("databases need", banner.TextContent); + Assert.Contains("2 databases need attention", banner.TextContent); + Assert.Equal("Open Settings", component.Find("aside.banner-attention button.banner-action").TextContent.Trim()); + Assert.Single(component.FindAll("aside.banner-attention button.banner-dismiss")); } [Fact] @@ -170,7 +172,7 @@ public async Task BannerHost_CancelUpgradeClicked_InvokesCancelDelegate() Cancel: () => cancelInvocationCount++)); var component = Render(); - await component.Find("aside.banner-upgrade-progress button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-upgrade-progress button.banner-action").ClickAsync(new MouseEventArgs()); Assert.Equal(1, cancelInvocationCount); } @@ -190,7 +192,7 @@ public async Task BannerHost_CancelUpgradeThrows_LogsViaTraceLogger_DoesNotPropa Cancel: () => throw new InvalidOperationException("cts disposed"))); var component = Render(); - await component.Find("aside.banner-upgrade-progress button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-upgrade-progress button.banner-action").ClickAsync(new MouseEventArgs()); Assert.Single(component.FindAll("aside.banner-upgrade-progress")); _bannerService.DidNotReceive().ReportCritical(Arg.Any()); @@ -295,12 +297,12 @@ public async Task BannerHost_CycleNextAtLast_DisabledAndDoesNotAdvance() var component = Render(); // Advance to last item (index 1). - await component.Find("button.banner-cycle-next").ClickAsync(new()); + await component.Find("button.banner-cycle-next").ClickAsync(new MouseEventArgs()); var next = component.Find("button.banner-cycle-next"); Assert.True(next.HasAttribute("disabled")); - await next.ClickAsync(new()); + await next.ClickAsync(new MouseEventArgs()); // Index stays at 1. Assert.Equal("2 of 2", component.Find(".banner-pagination").TextContent.Trim()); @@ -315,7 +317,7 @@ public async Task BannerHost_CycleNextClicked_AdvancesToAttentionItem() _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]); var component = Render(); - await component.Find("button.banner-cycle-next").ClickAsync(new()); + await component.Find("button.banner-cycle-next").ClickAsync(new MouseEventArgs()); var pagination = component.Find(".banner-pagination"); Assert.Equal("2 of 2", pagination.TextContent.Trim()); @@ -334,7 +336,7 @@ public async Task BannerHost_CyclePrevAtFirst_DisabledAndDoesNotAdvance() var prev = component.Find("button.banner-cycle-prev"); Assert.True(prev.HasAttribute("disabled")); - await prev.ClickAsync(new()); + await prev.ClickAsync(new MouseEventArgs()); // Index stays at 0 — first error still rendered. Assert.Equal("1 of 2", component.Find(".banner-pagination").TextContent.Trim()); @@ -355,7 +357,7 @@ public async Task BannerHost_CycleStableSelection_DismissingPrecedingError_Stays var component = Render(); // Advance to e1. - await component.Find("button.banner-cycle-next").ClickAsync(new()); + await component.Find("button.banner-cycle-next").ClickAsync(new MouseEventArgs()); Assert.Contains("Second: second message", component.Find("aside.banner-error").TextContent); Assert.Equal("2 of 3", component.Find(".banner-pagination").TextContent.Trim()); @@ -383,7 +385,7 @@ public async Task BannerHost_DismissErrorClicked_CallsDismissErrorWithEntryId() _bannerService.ErrorBanners.Returns([entry]); var component = Render(); - await component.Find("aside.banner-error button.banner-dismiss").ClickAsync(new()); + await component.Find("aside.banner-error button.banner-dismiss").ClickAsync(new MouseEventArgs()); _bannerService.Received(1).DismissError(entry.Id); } @@ -395,7 +397,7 @@ public async Task BannerHost_DismissInfoClicked_CallsDismissInfoBannerWithEntryI _bannerService.InfoBanners.Returns([info]); var component = Render(); - await component.Find("aside.banner-info button.banner-dismiss").ClickAsync(new()); + await component.Find("aside.banner-info button.banner-dismiss").ClickAsync(new MouseEventArgs()); _bannerService.Received(1).DismissInfoBanner(info.Id); } @@ -414,7 +416,7 @@ public async Task BannerHost_ErrorBannerActionClicked_InvokesSuppliedCallback() _bannerService.ErrorBanners.Returns([entry]); var component = Render(); - await component.Find("aside.banner-error button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-error button.banner-action").ClickAsync(new MouseEventArgs()); Assert.Equal(1, actionInvocationCount); } @@ -438,7 +440,7 @@ public async Task BannerHost_ErrorBannerActionThrows_LogsViaTraceLogger_DoesNotP var component = Render(); // Act - await component.Find("aside.banner-error button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-error button.banner-action").ClickAsync(new MouseEventArgs()); // Assert — banner stays visible, the critical slot was not populated, and the exception was logged. Assert.Single(component.FindAll("aside.banner-error")); @@ -524,7 +526,7 @@ public async Task BannerHost_OpenSettingsClicked_DismissesAttention_BeforeAwaiti _menuActionService.OpenSettingsAsync().Returns(Task.FromResult(true)); var component = Render(); - await component.Find("aside.banner-attention button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-attention button.banner-action").ClickAsync(new MouseEventArgs()); Received.InOrder( () => @@ -545,7 +547,7 @@ public async Task BannerHost_OpenSettingsReturnsFalse_DismissesAttentionImmediat _menuActionService.OpenSettingsAsync().Returns(Task.FromResult(false)); var component = Render(); - await component.Find("aside.banner-attention button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-attention button.banner-action").ClickAsync(new MouseEventArgs()); _bannerService.Received(1).DismissAttention(); _bannerService.Received(1) @@ -583,7 +585,7 @@ public async Task BannerHost_OpenSettingsReturnsFalse_RendersNewErrorBanner_NotS var component = Render(); Assert.Single(component.FindAll("aside.banner-attention")); - await component.Find("aside.banner-attention button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-attention button.banner-action").ClickAsync(new MouseEventArgs()); // Real BannerService raises StateChanged from inside ReportError; the mock does not, so raise it here // to drive the re-render that proves _selectedItem was steered to the new error. _bannerService.StateChanged += Raise.Event(); @@ -604,10 +606,10 @@ public async Task BannerHost_OpenSettingsThrowsJSDisconnected_DismissesAttention // before the await so the attention banner is already gone by the time the throw lands. _bannerService.AttentionEntries.Returns([BuildDatabaseEntry("a.db")]); _menuActionService.OpenSettingsAsync() - .Returns(Task.FromException(new Microsoft.JSInterop.JSDisconnectedException("circuit gone"))); + .Returns(Task.FromException(new JSDisconnectedException("circuit gone"))); var component = Render(); - await component.Find("aside.banner-attention button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-attention button.banner-action").ClickAsync(new MouseEventArgs()); _bannerService.Received(1).DismissAttention(); _bannerService.DidNotReceive().ReportError(Arg.Any(), Arg.Any()); @@ -625,7 +627,7 @@ public async Task BannerHost_OpenSettingsThrowsUnexpectedly_DismissesAttention_L _menuActionService.OpenSettingsAsync().Returns(Task.FromException(openException)); var component = Render(); - await component.Find("aside.banner-attention button.banner-action").ClickAsync(new()); + await component.Find("aside.banner-attention button.banner-action").ClickAsync(new MouseEventArgs()); _bannerService.Received(1).DismissAttention(); _bannerService.Received(1) @@ -642,7 +644,7 @@ public async Task BannerHost_RecoveryThrows_ShowsRecoveryFailureSubtitle() _bannerService.TryRecoverAsync().Returns(Task.FromException(new InvalidOperationException("recovery failed"))); var component = Render(); - await component.Find("aside.banner-critical .banner-actions button:nth-child(1)").ClickAsync(new()); + await component.Find("aside.banner-critical .banner-actions button:nth-child(1)").ClickAsync(new MouseEventArgs()); var subtitle = component.Find("aside.banner-critical .banner-feedback .banner-subtitle"); Assert.Contains("Recovery failed", subtitle.TextContent); @@ -656,7 +658,7 @@ public async Task BannerHost_RelaunchClicked_InvokesTryRestartAsync() _applicationRestartService.TryRestartAsync().Returns(true); var component = Render(); - await component.Find("aside.banner-critical .banner-actions button:nth-child(2)").ClickAsync(new()); + await component.Find("aside.banner-critical .banner-actions button:nth-child(2)").ClickAsync(new MouseEventArgs()); await _applicationRestartService.Received(1).TryRestartAsync(); } @@ -668,7 +670,7 @@ public async Task BannerHost_RelaunchFails_ShowsRestartFailureSubtitle() _applicationRestartService.TryRestartAsync().Returns(false); var component = Render(); - await component.Find("aside.banner-critical .banner-actions button:nth-child(2)").ClickAsync(new()); + await component.Find("aside.banner-critical .banner-actions button:nth-child(2)").ClickAsync(new MouseEventArgs()); var subtitle = component.Find("aside.banner-critical .banner-feedback .banner-subtitle"); Assert.Contains("Restart failed", subtitle.TextContent); @@ -681,7 +683,7 @@ public async Task BannerHost_ReloadClicked_InvokesTryRecoverAsync() _bannerService.TryRecoverAsync().Returns(Task.CompletedTask); var component = Render(); - await component.Find("aside.banner-critical .banner-actions button:nth-child(1)").ClickAsync(new()); + await component.Find("aside.banner-critical .banner-actions button:nth-child(1)").ClickAsync(new MouseEventArgs()); await _bannerService.Received(1).TryRecoverAsync(); } diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs index 1f8fc95a..120396a2 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs @@ -5,6 +5,8 @@ using EventLogExpert.Components.Database; using EventLogExpert.UI; using EventLogExpert.UI.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; namespace EventLogExpert.Components.Tests.Database; @@ -26,7 +28,7 @@ public async Task RemoveButtonClick_InvokesOnRemove() .Add(p => p.OnRemove, () => invocationCount++)); // Act - await component.Find(".db-entry-remove-btn").ClickAsync(new()); + await component.Find(".db-entry-remove-btn").ClickAsync(new MouseEventArgs()); // Assert Assert.Equal(1, invocationCount); @@ -237,34 +239,34 @@ public void Render_ReadyEnabledEntry_RendersTrashButton() } [Fact] - public void Render_ReadyEntry_ShowsToggle_AndNoBadge() + public void Render_ReadyEntryWithClassificationPending_ShowsDisabledToggle() { // Arrange var entry = MakeEntry(DatabaseStatus.Ready); // Act - var component = RenderRow(entry); + var component = RenderRow(entry, isClassificationPending: true); // Assert - Assert.Single(component.FindAll(".toggle")); - Assert.Empty(component.FindAll(".db-entry-badge")); - Assert.Empty(component.FindAll(".db-entry-upgrade-btn")); - Assert.Empty(component.FindAll(".db-entry-upgrading")); + var radios = component.FindAll(".toggle input[type='radio']"); + Assert.NotEmpty(radios); + Assert.All(radios, r => Assert.True(r.HasAttribute("disabled"))); } [Fact] - public void Render_ReadyEntryWithClassificationPending_ShowsDisabledToggle() + public void Render_ReadyEntry_ShowsToggle_AndNoBadge() { // Arrange var entry = MakeEntry(DatabaseStatus.Ready); // Act - var component = RenderRow(entry, isClassificationPending: true); + var component = RenderRow(entry); // Assert - var radios = component.FindAll(".toggle input[type='radio']"); - Assert.NotEmpty(radios); - Assert.All(radios, r => Assert.True(r.HasAttribute("disabled"))); + Assert.Single(component.FindAll(".toggle")); + Assert.Empty(component.FindAll(".db-entry-badge")); + Assert.Empty(component.FindAll(".db-entry-upgrade-btn")); + Assert.Empty(component.FindAll(".db-entry-upgrading")); } [Fact] @@ -367,7 +369,7 @@ public async Task RetryButtonClick_InvokesOnUpgrade() .Add(p => p.OnUpgrade, () => invocationCount++)); // Act - await component.Find(".db-entry-upgrade-btn").ClickAsync(new()); + await component.Find(".db-entry-upgrade-btn").ClickAsync(new MouseEventArgs()); // Assert Assert.Equal(1, invocationCount); @@ -386,7 +388,7 @@ public async Task TogglingTrueRadio_InvokesOnToggle_OnReadyEntry() // Act var enableRadio = component.FindAll(".toggle input[type='radio']")[1]; - await enableRadio.ChangeAsync(new() { Value = "true" }); + await enableRadio.ChangeAsync(new ChangeEventArgs { Value = "true" }); // Assert Assert.Equal(1, invocationCount); @@ -403,7 +405,7 @@ public async Task UpgradeButtonClick_InvokesOnUpgrade() .Add(p => p.OnUpgrade, () => invocationCount++)); // Act - await component.Find(".db-entry-upgrade-btn").ClickAsync(new()); + await component.Find(".db-entry-upgrade-btn").ClickAsync(new MouseEventArgs()); // Assert Assert.Equal(1, invocationCount); diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs index 3a82ee22..b5369c55 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs @@ -1,12 +1,16 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using AngleSharp.Dom; +using AngleSharp.Html.Dom; using Bunit; using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -50,7 +54,7 @@ public async Task DatabaseRecoveryDialog_AllRowsSucceed_AutoDismisses() .Add(p => p.OnDismissed, () => Interlocked.Increment(ref _onDismissedCallCount))); // Act - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); _databaseService.Entries.Returns([]); _databaseService.EntriesChanged += Raise.Event(_databaseService, EventArgs.Empty); @@ -73,8 +77,8 @@ public async Task DatabaseRecoveryDialog_ApplyDeleteReturnsFalse_SurfacesErrorBa .Add(p => p.OnDismissed, () => Interlocked.Increment(ref _onDismissedCallCount))); // Act - await component.Find("button.button:contains('Delete all')").ClickAsync(new()); - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button.button:contains('Delete all')").ClickAsync(new MouseEventArgs()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert _bannerService.Received(1).ReportError( @@ -98,17 +102,17 @@ public async Task DatabaseRecoveryDialog_ApplyDisablesAllControls_WhilePending() var component = Render(); // Act - var applyClick = component.Find("button:contains('Apply')").ClickAsync(new()); + var applyClick = component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert - Assert.True(((AngleSharp.Html.Dom.IHtmlButtonElement)component.Find("button:contains('Apply')")).IsDisabled); - Assert.True(((AngleSharp.Html.Dom.IHtmlButtonElement)component.Find("button:contains('Cancel')")).IsDisabled); - Assert.True(((AngleSharp.Html.Dom.IHtmlButtonElement)component.Find("button.button:contains('Restore all')")).IsDisabled); - Assert.True(((AngleSharp.Html.Dom.IHtmlButtonElement)component.Find("button.button:contains('Delete all')")).IsDisabled); + Assert.True(((IHtmlButtonElement)component.Find("button:contains('Apply')")).IsDisabled); + Assert.True(((IHtmlButtonElement)component.Find("button:contains('Cancel')")).IsDisabled); + Assert.True(((IHtmlButtonElement)component.Find("button.button:contains('Restore all')")).IsDisabled); + Assert.True(((IHtmlButtonElement)component.Find("button.button:contains('Delete all')")).IsDisabled); foreach (var radio in component.FindAll("li.recovery-row input[type=radio]")) { - Assert.True(((AngleSharp.Html.Dom.IHtmlInputElement)radio).IsDisabled); + Assert.True(((IHtmlInputElement)radio).IsDisabled); } pendingRestore.SetResult(true); @@ -131,9 +135,9 @@ public async Task DatabaseRecoveryDialog_ApplyMixed_CallsBothMethodsForRespectiv // Act var bRow = component.FindAll("li.recovery-row")[1]; var bDeleteRadio = bRow.QuerySelectorAll("input[type=radio]")[1]; - await bDeleteRadio.ChangeAsync(new() { Value = "on" }); + await bDeleteRadio.ChangeAsync(new ChangeEventArgs { Value = "on" }); - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert await _databaseService.Received(1).RestoreFromBackupAsync("a.db", Arg.Any()); @@ -154,7 +158,7 @@ public async Task DatabaseRecoveryDialog_ApplyRestoreReturnsFalse_SurfacesErrorB .Add(p => p.OnDismissed, () => Interlocked.Increment(ref _onDismissedCallCount))); // Act - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert _bannerService.Received(1).ReportError( @@ -177,7 +181,7 @@ public async Task DatabaseRecoveryDialog_ApplyThrowsInvalidOperation_TreatsAsBen var component = Render(); // Act - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert _bannerService.DidNotReceive().ReportError( @@ -201,7 +205,7 @@ public async Task DatabaseRecoveryDialog_ApplyThrowsUnexpected_SurfacesErrorBann var component = Render(); // Act - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert _bannerService.Received(1).ReportError( @@ -223,8 +227,8 @@ public async Task DatabaseRecoveryDialog_ApplyWithDelete_CallsDeleteEntryWithBac var component = Render(); // Act - await component.Find("button.button:contains('Delete all')").ClickAsync(new()); - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button.button:contains('Delete all')").ClickAsync(new MouseEventArgs()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert await _databaseService.Received(1).DeleteEntryWithBackupAsync( @@ -246,7 +250,7 @@ public async Task DatabaseRecoveryDialog_ApplyWithRestore_CallsRestoreFromBackup var component = Render(); // Act - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert await _databaseService.Received(1).RestoreFromBackupAsync( @@ -267,7 +271,7 @@ public async Task DatabaseRecoveryDialog_CancelClicked_RaisesOnDismissedDoesNotC .Add(p => p.OnDismissed, () => Interlocked.Increment(ref _onDismissedCallCount))); // Act - await component.Find("button:contains('Cancel')").ClickAsync(new()); + await component.Find("button:contains('Cancel')").ClickAsync(new MouseEventArgs()); // Assert Assert.Equal(1, _onDismissedCallCount); @@ -294,8 +298,8 @@ public void DatabaseRecoveryDialog_DefaultsEachRowToRestore() { var radios = row.QuerySelectorAll("input[type=radio]"); Assert.Equal(2, radios.Length); - Assert.True(((AngleSharp.Html.Dom.IHtmlInputElement)radios[0]).IsChecked); - Assert.False(((AngleSharp.Html.Dom.IHtmlInputElement)radios[1]).IsChecked); + Assert.True(((IHtmlInputElement)radios[0]).IsChecked); + Assert.False(((IHtmlInputElement)radios[1]).IsChecked); } } @@ -309,14 +313,14 @@ public async Task DatabaseRecoveryDialog_DeleteAllClicked_SetsAllRowsToDelete() var component = Render(); // Act - await component.Find("button.button:contains('Delete all')").ClickAsync(new()); + await component.Find("button.button:contains('Delete all')").ClickAsync(new MouseEventArgs()); // Assert foreach (var row in component.FindAll("li.recovery-row")) { var radios = row.QuerySelectorAll("input[type=radio]"); - Assert.False(((AngleSharp.Html.Dom.IHtmlInputElement)radios[0]).IsChecked); - Assert.True(((AngleSharp.Html.Dom.IHtmlInputElement)radios[1]).IsChecked); + Assert.False(((IHtmlInputElement)radios[0]).IsChecked); + Assert.True(((IHtmlInputElement)radios[1]).IsChecked); } } @@ -357,8 +361,8 @@ public async Task DatabaseRecoveryDialog_EntriesChangedNewBackupExistsEntry_Adds Assert.Contains("b.db", newRow.TextContent); var radios = newRow.QuerySelectorAll("input[type=radio]"); - Assert.True(((AngleSharp.Html.Dom.IHtmlInputElement)radios[0]).IsChecked); - Assert.False(((AngleSharp.Html.Dom.IHtmlInputElement)radios[1]).IsChecked); + Assert.True(((IHtmlInputElement)radios[0]).IsChecked); + Assert.False(((IHtmlInputElement)radios[1]).IsChecked); await Task.CompletedTask; } @@ -375,7 +379,7 @@ public async Task DatabaseRecoveryDialog_EntriesChangedSubsetResolved_RemovesRes var bRow = component.FindAll("li.recovery-row")[1]; var bDeleteRadio = bRow.QuerySelectorAll("input[type=radio]")[1]; - await bDeleteRadio.ChangeAsync(new() { Value = "on" }); + await bDeleteRadio.ChangeAsync(new ChangeEventArgs { Value = "on" }); // Act _databaseService.Entries.Returns([BuildEntry("a.db", backupExists: true)]); @@ -388,8 +392,8 @@ public async Task DatabaseRecoveryDialog_EntriesChangedSubsetResolved_RemovesRes Assert.Contains("a.db", remainingRow.TextContent); var radios = remainingRow.QuerySelectorAll("input[type=radio]"); - Assert.True(((AngleSharp.Html.Dom.IHtmlInputElement)radios[0]).IsChecked); - Assert.False(((AngleSharp.Html.Dom.IHtmlInputElement)radios[1]).IsChecked); + Assert.True(((IHtmlInputElement)radios[0]).IsChecked); + Assert.False(((IHtmlInputElement)radios[1]).IsChecked); Assert.Equal(0, _onDismissedCallCount); } @@ -406,7 +410,7 @@ public async Task DatabaseRecoveryDialog_EscDuringApply_DoesNotDismiss() .Add(p => p.OnDismissed, () => Interlocked.Increment(ref _onDismissedCallCount))); // Act - var applyTask = component.Find("button:contains('Apply')").ClickAsync(new()); + var applyTask = component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); await component.Find("dialog").TriggerEventAsync("oncancel", EventArgs.Empty); @@ -430,7 +434,7 @@ public async Task DatabaseRecoveryDialog_FailedRow_LosesFailureMarkOnNextApplySu var component = Render(); - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); Assert.Contains( "recovery-row-failed", @@ -443,7 +447,7 @@ public async Task DatabaseRecoveryDialog_FailedRow_LosesFailureMarkOnNextApplySu _databaseService.RestoreFromBackupAsync("a.db", Arg.Any()) .Returns(Task.FromResult(true)); - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert Assert.DoesNotContain( @@ -503,18 +507,18 @@ public async Task DatabaseRecoveryDialog_RestoreAllClicked_SetsAllRowsToRestore( { var deleteRadio = component.FindAll("li.recovery-row")[rowIndex] .QuerySelectorAll("input[type=radio]")[1]; - await deleteRadio.ChangeAsync(new() { Value = "on" }); + await deleteRadio.ChangeAsync(new ChangeEventArgs { Value = "on" }); } // Act - await component.Find("button.button:contains('Restore all')").ClickAsync(new()); + await component.Find("button.button:contains('Restore all')").ClickAsync(new MouseEventArgs()); // Assert foreach (var row in component.FindAll("li.recovery-row")) { var radios = row.QuerySelectorAll("input[type=radio]"); - Assert.True(((AngleSharp.Html.Dom.IHtmlInputElement)radios[0]).IsChecked); - Assert.False(((AngleSharp.Html.Dom.IHtmlInputElement)radios[1]).IsChecked); + Assert.True(((IHtmlInputElement)radios[0]).IsChecked); + Assert.False(((IHtmlInputElement)radios[1]).IsChecked); } } @@ -543,7 +547,7 @@ public async Task DatabaseRecoveryDialog_RowResolvedExternallyMidLoop_DoesNotCal _databaseService.Entries.Returns(entriesAfter); // Act - await component.Find("button:contains('Apply')").ClickAsync(new()); + await component.Find("button:contains('Apply')").ClickAsync(new MouseEventArgs()); // Assert await _databaseService.Received(1).RestoreFromBackupAsync("a.db", Arg.Any()); @@ -563,7 +567,7 @@ private static DatabaseEntry BuildEntry(string fileName, bool backupExists) => DatabaseStatus.UpgradeRequired, BackupExists: backupExists); - private static AngleSharp.Dom.IElement FindRowByFileName( + private static IElement FindRowByFileName( IRenderedComponent component, string fileName) { diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs index 8f19b9ac..a70aa161 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs @@ -7,6 +7,7 @@ using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -115,7 +116,7 @@ public async Task DatabaseRecoveryHost_DialogDismissed_BannerStillVisible_DoesNo await component.InvokeAsync(() => _capturedRecoveryAction!()); // Act - await component.Find("button:contains('Cancel')").ClickAsync(new()); + await component.Find("button:contains('Cancel')").ClickAsync(new MouseEventArgs()); // Assert Assert.Empty(component.FindAll("dialog")); diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs index 795d183c..e92ee3a5 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -49,7 +50,7 @@ public async Task SettingsUpgradeProgressBanner_CancelClicked_InvokesProgressCan // Act var component = Render(); - await component.Find("aside.settings-upgrade-banner button.banner-action").ClickAsync(new()); + await component.Find("aside.settings-upgrade-banner button.banner-action").ClickAsync(new MouseEventArgs()); // Assert Assert.Equal(1, cancelInvocationCount); @@ -64,7 +65,7 @@ public async Task SettingsUpgradeProgressBanner_CancelThrows_LogsViaTraceLoggerA // Act var component = Render(); - await component.Find("aside.settings-upgrade-banner button.banner-action").ClickAsync(new()); + await component.Find("aside.settings-upgrade-banner button.banner-action").ClickAsync(new MouseEventArgs()); // Assert Assert.Single(component.FindAll("aside.settings-upgrade-banner")); diff --git a/tests/Unit/EventLogExpert.Components.Tests/GlobalUsings.cs b/tests/Unit/EventLogExpert.Components.Tests/GlobalUsings.cs index c802f448..b04ace3e 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/GlobalUsings.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/GlobalUsings.cs @@ -1 +1,4 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + global using Xunit; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs index da358f8b..a2bcc03f 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs @@ -5,13 +5,18 @@ using EventLogExpert.Components.Modals; using EventLogExpert.Components.Tests.TestUtils; using EventLogExpert.Components.Tests.TestUtils.Constants; +using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using Fluxor; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NSubstitute; using NSubstitute.ExceptionExtensions; +using System.Runtime.CompilerServices; using System.Text; +using TestContext = Xunit.TestContext; namespace EventLogExpert.Components.Tests.Modals; @@ -21,6 +26,7 @@ public sealed class DebugLogModalTests : BunitContext private readonly IClipboardService _clipboardService = Substitute.For(); private readonly IFileLogger _fileLogger = Substitute.For(); private readonly IFileSaveService _fileSaveService = Substitute.For(); + private readonly IInlineAlertHostBroker _inlineAlertHostBroker = Substitute.For(); private readonly IModalService _modalService = Substitute.For(); public DebugLogModalTests() @@ -31,6 +37,7 @@ public DebugLogModalTests() Services.AddSingleton(_clipboardService); Services.AddSingleton(_fileLogger); Services.AddSingleton(_fileSaveService); + Services.AddSingleton(_inlineAlertHostBroker); Services.AddSingleton(_modalService); Services.AddFluxor(options => options.ScanAssemblies(typeof(DebugLogModal).Assembly)); @@ -134,7 +141,7 @@ await component.WaitForAssertionAsync( TimeSpan.FromSeconds(2)); // Act - await component.Find("button:contains('Clear')").ClickAsync(new()); + await component.Find("button:contains('Clear')").ClickAsync(new MouseEventArgs()); await component.WaitForAssertionAsync( () => Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()), @@ -142,7 +149,7 @@ await component.WaitForAssertionAsync( gate.SetResult(); - await Task.Delay(200, Xunit.TestContext.Current.CancellationToken); + await Task.Delay(200, TestContext.Current.CancellationToken); // Assert Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()); @@ -167,7 +174,7 @@ await component.WaitForAssertionAsync(() => Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act - await component.Find("button:contains('Clear')").ClickAsync(new()); + await component.Find("button:contains('Clear')").ClickAsync(new MouseEventArgs()); // Assert await _fileLogger.Received(1).ClearAsync(); @@ -198,7 +205,7 @@ await component.WaitForAssertionAsync(() => Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act - await component.Find("button:contains('Clear')").ClickAsync(new()); + await component.Find("button:contains('Clear')").ClickAsync(new MouseEventArgs()); // Assert: alert shown and counter unchanged (UI state preserved on failure) await _alertDialogService.Received(1).ShowAlert("Clear Failed", "permission denied", "OK"); @@ -207,12 +214,13 @@ await component.WaitForAssertionAsync(() => } [Fact] - public async Task DebugLogModal_CopyClick_CallsCopyTextAsyncWithEnvironmentNewLineJoinedDisplayed() + public async Task DebugLogModal_CopyClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed() { // Arrange var lines = new[] { DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage), + DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage), DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage), }; @@ -221,25 +229,25 @@ public async Task DebugLogModal_CopyClick_CallsCopyTextAsyncWithEnvironmentNewLi var component = Render(); await component.WaitForAssertionAsync(() => - Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - - var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] }); + Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - // Act - await component.Find("button:contains('Copy')").ClickAsync(new()); + // Act — typed text doesn't apply until the 250ms debounce; Copy must flush first. + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" }); + await component.Find("button:contains('Copy')").ClickAsync(new MouseEventArgs()); - // Assert - await _clipboardService.Received(1).CopyTextAsync(expectedContent); + // Assert: clipboard receives only the entry whose Message contains "error". + await _clipboardService.Received(1).CopyTextAsync(lines[1]); + await _clipboardService.DidNotReceive().CopyTextAsync( + Arg.Is(s => s.Contains(Constants.DebugLogFirstMessage))); } [Fact] - public async Task DebugLogModal_CopyClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed() + public async Task DebugLogModal_CopyClick_CallsCopyTextAsyncWithEnvironmentNewLineJoinedDisplayed() { // Arrange var lines = new[] { DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage), - DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage), DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage), }; @@ -248,16 +256,15 @@ public async Task DebugLogModal_CopyClickWhilePendingFilterPending_FlushesFilter var component = Render(); await component.WaitForAssertionAsync(() => - Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); + Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - // Act — typed text doesn't apply until the 250ms debounce; Copy must flush first. - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "error" }); - await component.Find("button:contains('Copy')").ClickAsync(new()); + var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] }); - // Assert: clipboard receives only the entry whose Message contains "error". - await _clipboardService.Received(1).CopyTextAsync(lines[1]); - await _clipboardService.DidNotReceive().CopyTextAsync( - Arg.Is(s => s.Contains(Constants.DebugLogFirstMessage))); + // Act + await component.Find("button:contains('Copy')").ClickAsync(new MouseEventArgs()); + + // Assert + await _clipboardService.Received(1).CopyTextAsync(expectedContent); } [Fact] @@ -299,7 +306,7 @@ await component.WaitForAssertionAsync( } [Fact] - public async Task DebugLogModal_EmptyLog_CopyButtonIsDisabled() + public async Task DebugLogModal_EmptyLogWithActiveFilter_StillShowsLogIsEmptyMessage() { // Arrange _fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([])); @@ -309,14 +316,19 @@ public async Task DebugLogModal_EmptyLog_CopyButtonIsDisabled() await component.WaitForAssertionAsync(() => Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - // Act + Assert - var copyButton = component.Find("button:contains('Copy')"); + // Act: type a filter while the log is empty + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "any-search-text" }); - Assert.True(copyButton.HasAttribute("disabled")); + // Assert — zero entries (not zero filtered); "No entries match filters" would mislead. + await component.WaitForAssertionAsync(() => + { + Assert.Contains("Log is Empty...", component.Markup); + Assert.DoesNotContain("No entries match filters.", component.Markup); + }); } [Fact] - public async Task DebugLogModal_EmptyLog_ExportButtonIsDisabled() + public async Task DebugLogModal_EmptyLog_CopyButtonIsDisabled() { // Arrange _fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([])); @@ -327,56 +339,52 @@ await component.WaitForAssertionAsync(() => Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act + Assert - var exportButton = component.Find("button:contains('Export')"); + var copyButton = component.Find("button:contains('Copy')"); - Assert.True(exportButton.HasAttribute("disabled")); + Assert.True(copyButton.HasAttribute("disabled")); } [Fact] - public async Task DebugLogModal_EmptyLog_ShowsLogIsEmptyMessageAndZeroCounter() + public async Task DebugLogModal_EmptyLog_ExportButtonIsDisabled() { // Arrange _fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([])); - // Act var component = Render(); - // Assert await component.WaitForAssertionAsync(() => Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - Assert.Contains("Log is Empty...", component.Markup); + // Act + Assert + var exportButton = component.Find("button:contains('Export')"); + + Assert.True(exportButton.HasAttribute("disabled")); } [Fact] - public async Task DebugLogModal_EmptyLogWithActiveFilter_StillShowsLogIsEmptyMessage() + public async Task DebugLogModal_EmptyLog_ShowsLogIsEmptyMessageAndZeroCounter() { // Arrange _fileLogger.LoadAsync(Arg.Any()).Returns(DebugLogUtils.ToAsyncEnumerable([])); + // Act var component = Render(); + // Assert await component.WaitForAssertionAsync(() => Assert.Equal("0 of 0 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - // Act: type a filter while the log is empty - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "any-search-text" }); - - // Assert — zero entries (not zero filtered); "No entries match filters" would mislead. - await component.WaitForAssertionAsync(() => - { - Assert.Contains("Log is Empty...", component.Markup); - Assert.DoesNotContain("No entries match filters.", component.Markup); - }); + Assert.Contains("Log is Empty...", component.Markup); } [Fact] - public async Task DebugLogModal_ExportClick_CallsSaveAsyncWithEnvironmentNewLineJoinedDisplayed() + public async Task DebugLogModal_ExportClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed() { // Arrange var lines = new[] { DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage), + DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage), DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage), }; @@ -393,31 +401,29 @@ public async Task DebugLogModal_ExportClick_CallsSaveAsyncWithEnvironmentNewLine var component = Render(); await component.WaitForAssertionAsync(() => - Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - - var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] }); + Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - // Act - await component.Find("button:contains('Export')").ClickAsync(new()); + // Act: type a filter that drops two entries, then click Export before debounce expires. + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" }); + await component.Find("button:contains('Export')").ClickAsync(new MouseEventArgs()); // Assert await _fileSaveService.Received(1).SaveAsync( - Arg.Is(name => name.StartsWith("debug-log-") && name.EndsWith(".log")), - FileSaveServiceFileTypes.Log, + Arg.Any(), + FileSaveFileTypes.Log, Arg.Any>()); Assert.NotNull(capturedWriter); - Assert.Equal(expectedContent, await InvokeWriterAndDecodeAsync(capturedWriter)); + Assert.Equal(lines[1], await InvokeWriterAndDecodeAsync(capturedWriter)); } [Fact] - public async Task DebugLogModal_ExportClickWhilePendingFilterPending_FlushesFilterBeforeReadingDisplayed() + public async Task DebugLogModal_ExportClick_CallsSaveAsyncWithEnvironmentNewLineJoinedDisplayed() { // Arrange var lines = new[] { DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogFirstMessage), - DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogErrorMessage), DebugLogUtils.BuildLine(LogLevel.Information, Constants.DebugLogSecondMessage), }; @@ -434,20 +440,21 @@ public async Task DebugLogModal_ExportClickWhilePendingFilterPending_FlushesFilt var component = Render(); await component.WaitForAssertionAsync(() => - Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); + Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); - // Act: type a filter that drops two entries, then click Export before debounce expires. - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "error" }); - await component.Find("button:contains('Export')").ClickAsync(new()); + var expectedContent = string.Join(Environment.NewLine, new[] { lines[1], lines[0] }); + + // Act + await component.Find("button:contains('Export')").ClickAsync(new MouseEventArgs()); // Assert await _fileSaveService.Received(1).SaveAsync( - Arg.Any(), - FileSaveServiceFileTypes.Log, + Arg.Is(name => name.StartsWith("debug-log-") && name.EndsWith(".log")), + FileSaveFileTypes.Log, Arg.Any>()); Assert.NotNull(capturedWriter); - Assert.Equal(lines[1], await InvokeWriterAndDecodeAsync(capturedWriter)); + Assert.Equal(expectedContent, await InvokeWriterAndDecodeAsync(capturedWriter)); } [Fact] @@ -469,7 +476,7 @@ await component.WaitForAssertionAsync(() => Assert.Equal("1 of 1 entry", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act - await component.Find("button:contains('Export')").ClickAsync(new()); + await component.Find("button:contains('Export')").ClickAsync(new MouseEventArgs()); // Assert await _alertDialogService.DidNotReceive().ShowAlert( @@ -495,7 +502,7 @@ await component.WaitForAssertionAsync(() => Assert.Equal("1 of 1 entry", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act - await component.Find("button:contains('Export')").ClickAsync(new()); + await component.Find("button:contains('Export')").ClickAsync(new MouseEventArgs()); // Assert await _alertDialogService.Received(1).ShowAlert("Export Failed", "disk full", "OK"); @@ -519,7 +526,7 @@ await component.WaitForAssertionAsync(() => Assert.Equal("2 of 2 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "no-such-text" }); + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "no-such-text" }); // Assert await component.WaitForAssertionAsync( @@ -583,7 +590,7 @@ await component.WaitForAssertionAsync( var errorOption = levelDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Error"); - await errorOption.MouseDownAsync(new()); + await errorOption.MouseDownAsync(new MouseEventArgs()); await component.WaitForAssertionAsync( () => Assert.Equal("0 of 99 entries", component.Find(".debug-log-footer-counter").TextContent.Trim()), @@ -618,13 +625,13 @@ await component.WaitForAssertionAsync(() => Assert.Equal("5 of 5 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act — type a string filter (debounce starts), then change the level dropdown before it elapses. - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "matching" }); + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "matching" }); var levelDropdown = component.Find("input[aria-label='Level']").ParentElement!; var errorOption = levelDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Error"); - await errorOption.MouseDownAsync(new()); + await errorOption.MouseDownAsync(new MouseEventArgs()); // Assert — only the entry that matches BOTH filters survives. await component.WaitForAssertionAsync(() => @@ -654,7 +661,7 @@ await component.WaitForAssertionAsync(() => var errorOption = levelDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Error"); - await errorOption.MouseDownAsync(new()); + await errorOption.MouseDownAsync(new MouseEventArgs()); // Assert await component.WaitForAssertionAsync(() => @@ -684,7 +691,7 @@ await component.WaitForAssertionAsync(() => var multiSelectOption = operatorDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Multi Select"); - await multiSelectOption.MouseDownAsync(new()); + await multiSelectOption.MouseDownAsync(new MouseEventArgs()); // Then pick Error and Warning from the multi-select dropdown var levelsDropdown = component.Find("input[aria-label='Levels']").ParentElement!; @@ -692,12 +699,12 @@ await component.WaitForAssertionAsync(() => var errorOption = levelsDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Error"); - await errorOption.MouseDownAsync(new()); + await errorOption.MouseDownAsync(new MouseEventArgs()); var warningOption = levelsDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Warning"); - await warningOption.MouseDownAsync(new()); + await warningOption.MouseDownAsync(new MouseEventArgs()); // Assert: Error and Warning remain; Information is filtered out await component.WaitForAssertionAsync(() => @@ -727,14 +734,14 @@ await component.WaitForAssertionAsync(() => var notEqualOption = operatorDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Not Equal"); - await notEqualOption.MouseDownAsync(new()); + await notEqualOption.MouseDownAsync(new MouseEventArgs()); // Then select "Information" as the value to exclude var levelDropdown = component.Find("input[aria-label='Level']").ParentElement!; var informationOption = levelDropdown.QuerySelectorAll("[role='option']") .First(o => o.TextContent.Trim() == "Information"); - await informationOption.MouseDownAsync(new()); + await informationOption.MouseDownAsync(new MouseEventArgs()); // Assert: Error and Warning remain (Information is the only level excluded) await component.WaitForAssertionAsync(() => @@ -809,8 +816,8 @@ await component.WaitForAssertionAsync(() => Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act — type a filter (debounce starts), then click Refresh before it elapses. - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "matching" }); - await component.Find("button:contains('Refresh')").ClickAsync(new()); + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "matching" }); + await component.Find("button:contains('Refresh')").ClickAsync(new MouseEventArgs()); // Assert — the reload re-projects with the just-typed filter, not the prior empty one. await component.WaitForAssertionAsync(() => @@ -860,7 +867,7 @@ await component.WaitForAssertionAsync(() => Assert.Equal("3 of 3 entries", component.Find(".debug-log-footer-counter").TextContent.Trim())); // Act - await component.Find("input[aria-label='Filter messages']").InputAsync(new() { Value = "error" }); + await component.Find("input[aria-label='Filter messages']").InputAsync(new ChangeEventArgs { Value = "error" }); // Assert (wait long enough for the 250ms debounce + projection) await component.WaitForAssertionAsync( @@ -884,13 +891,13 @@ public async Task DebugLogModal_WhenRefreshIsRestartedWhileLoading_CancelsInFlig var component = Render(); - await firstStartedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), Xunit.TestContext.Current.CancellationToken); + await firstStartedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Act - clicking Refresh starts a new load, which cancels the in-flight one - await component.Find(".debug-log-footer-right button:first-child").ClickAsync(new()); + await component.Find(".debug-log-footer-right button:first-child").ClickAsync(new MouseEventArgs()); // Assert - await cancellationObservedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), Xunit.TestContext.Current.CancellationToken); + await cancellationObservedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); await component.WaitForAssertionAsync(() => Assert.Equal("1 of 1 entry", component.Find(".debug-log-footer-counter").TextContent.Trim())); @@ -918,9 +925,9 @@ public async Task DebugLogModal_WhenStaleProviderEmitsAfterRestart_DoesNotMutate var component = Render(); - await firstStartedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), Xunit.TestContext.Current.CancellationToken); + await firstStartedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); - await component.Find(".debug-log-footer-right button:first-child").ClickAsync(new()); + await component.Find(".debug-log-footer-right button:first-child").ClickAsync(new MouseEventArgs()); await component.WaitForAssertionAsync(() => Assert.Equal("1 of 1 entry", component.Find(".debug-log-footer-counter").TextContent.Trim())); @@ -928,7 +935,7 @@ await component.WaitForAssertionAsync(() => // Release stale provider; guard should dispose after next yield (signalled via finally). releaseStaleEmissionTcs.TrySetResult(); - await staleDisposedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), Xunit.TestContext.Current.CancellationToken); + await staleDisposedTcs.Task.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // Counter unchanged; orphaned stale Refresh cannot mutate visible state. Assert.Equal("1 of 1 entry", component.Find(".debug-log-footer-counter").TextContent.Trim()); @@ -938,7 +945,7 @@ await component.WaitForAssertionAsync(() => } private static async IAsyncEnumerable HangingSource( - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken, + [EnumeratorCancellation] CancellationToken cancellationToken, TaskCompletionSource startedTcs, TaskCompletionSource cancelledTcs) { diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/GlobalUsings.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/GlobalUsings.cs index c802f448..b04ace3e 100644 --- a/tests/Unit/EventLogExpert.EventDbTool.Tests/GlobalUsings.cs +++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/GlobalUsings.cs @@ -1 +1,4 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + global using Xunit; diff --git a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs b/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs index 0996e7cc..cdec3c49 100644 --- a/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs +++ b/tests/Unit/EventLogExpert.EventDbTool.Tests/TestUtils/Constants/Constants.Database.cs @@ -6,7 +6,7 @@ namespace EventLogExpert.EventDbTool.Tests.TestUtils.Constants; public sealed partial class Constants { public const string FirstProviderName = "First-Provider"; + public const string OwningPublisherName = "Owning-Publisher-Name"; public const string SecondProviderName = "Second-Provider"; public const string SharedProviderName = "Shared-Provider"; - public const string OwningPublisherName = "Owning-Publisher-Name"; } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs index 0f2192fc..1e039660 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Common/Databases/DatabasePathSorterTests.cs @@ -49,6 +49,19 @@ public void Sort_WithEmptyList_ShouldReturnEmptyList() Assert.Empty(sorted); } + [Fact] + public void Sort_WithFileNamesOnly_PreservesNames() + { + var fileNames = new[] { "Exchange 2019.db", "Exchange 2016.db", "Windows 2019.db" }; + + var sorted = DatabasePathSorter.Sort(fileNames); + + Assert.Equal(3, sorted.Count); + Assert.Equal("Exchange 2019.db", sorted[0]); + Assert.Equal("Exchange 2016.db", sorted[1]); + Assert.Equal("Windows 2019.db", sorted[2]); + } + [Fact] public void Sort_WithMixedVersionsAndNoVersions_ShouldSortCorrectly() { @@ -135,17 +148,4 @@ public void Sort_WithSingleDatabase_ShouldReturnSameDatabase() Assert.Single(sorted); Assert.Equal(@"C:\Test\Windows 2019.db", sorted[0]); } - - [Fact] - public void Sort_WithFileNamesOnly_PreservesNames() - { - var fileNames = new[] { "Exchange 2019.db", "Exchange 2016.db", "Windows 2019.db" }; - - var sorted = DatabasePathSorter.Sort(fileNames); - - Assert.Equal(3, sorted.Count); - Assert.Equal("Exchange 2019.db", sorted[0]); - Assert.Equal("Exchange 2016.db", sorted[1]); - Assert.Equal("Windows 2019.db", sorted[2]); - } } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/GlobalUsings.cs b/tests/Unit/EventLogExpert.Eventing.Tests/GlobalUsings.cs index c802f448..b04ace3e 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/GlobalUsings.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/GlobalUsings.cs @@ -1 +1,4 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + global using Xunit; diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs index 062ebd08..291df175 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Interop/NativeMethodsEvtTests.cs @@ -92,19 +92,18 @@ public void ConvertVariant_WhenBooleanTrue_ShouldReturnTrue() } [Fact] - public void ConvertVariant_WhenByte_ShouldReturnByte() + public void ConvertVariant_WhenByteArrayEmpty_ShouldReturnEmptyArray() { // Arrange - byte expectedValue = 255; - var variant = CreateVariant(EvtVariantType.Byte, expectedValue); + var variant = CreateVariantWithCount(EvtVariantType.ByteArray, IntPtr.Zero, 0); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Equal(expectedValue, result); + Assert.IsType(result); + Assert.Empty((byte[])result); } [Fact] @@ -135,18 +134,19 @@ public void ConvertVariant_WhenByteArray_ShouldReturnByteArray() } [Fact] - public void ConvertVariant_WhenByteArrayEmpty_ShouldReturnEmptyArray() + public void ConvertVariant_WhenByte_ShouldReturnByte() { // Arrange - var variant = CreateVariantWithCount(EvtVariantType.ByteArray, IntPtr.Zero, 0); + byte expectedValue = 255; + var variant = CreateVariant(EvtVariantType.Byte, expectedValue); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Empty((byte[])result); + Assert.IsType(result); + Assert.Equal(expectedValue, result); } [Theory] @@ -231,19 +231,18 @@ public void ConvertVariant_WhenGuid_ShouldReturnGuid() } [Fact] - public void ConvertVariant_WhenHexInt32_ShouldReturnInt32() + public void ConvertVariant_WhenHexInt32ArrayEmpty_ShouldReturnEmptyArray() { // Arrange - int expectedValue = unchecked((int)0x1234ABCD); - var variant = CreateVariant(EvtVariantType.HexInt32, expectedValue); + var variant = CreateVariantWithCount(EvtVariantType.HexInt32Array, IntPtr.Zero, 0); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Equal(expectedValue, result); + Assert.IsType(result); + Assert.Empty((int[])result); } [Fact] @@ -278,18 +277,19 @@ public void ConvertVariant_WhenHexInt32Array_ShouldReturnInt32Array() } [Fact] - public void ConvertVariant_WhenHexInt32ArrayEmpty_ShouldReturnEmptyArray() + public void ConvertVariant_WhenHexInt32_ShouldReturnInt32() { // Arrange - var variant = CreateVariantWithCount(EvtVariantType.HexInt32Array, IntPtr.Zero, 0); + int expectedValue = unchecked((int)0x1234ABCD); + var variant = CreateVariant(EvtVariantType.HexInt32, expectedValue); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Empty((int[])result); + Assert.IsType(result); + Assert.Equal(expectedValue, result); } [Fact] @@ -463,28 +463,18 @@ public void ConvertVariant_WhenSizeT_ShouldReturnIntPtr() } [Fact] - public void ConvertVariant_WhenString_ShouldReturnString() + public void ConvertVariant_WhenStringArrayEmpty_ShouldReturnEmptyArray() { // Arrange - var testString = "Test String"; - IntPtr stringPtr = Marshal.StringToHGlobalUni(testString); - - try - { - var variant = CreateVariant(EvtVariantType.String, stringPtr); + var variant = CreateVariantWithCount(EvtVariantType.StringArray, IntPtr.Zero, 0); - // Act - var result = NativeMethods.ConvertVariant(variant); + // Act + var result = NativeMethods.ConvertVariant(variant); - // Assert - Assert.NotNull(result); - Assert.IsType(result); - Assert.Equal(testString, result); - } - finally - { - Marshal.FreeHGlobal(stringPtr); - } + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Empty((string[])result); } [Fact] @@ -528,18 +518,28 @@ public void ConvertVariant_WhenStringArray_ShouldReturnStringArray() } [Fact] - public void ConvertVariant_WhenStringArrayEmpty_ShouldReturnEmptyArray() + public void ConvertVariant_WhenString_ShouldReturnString() { // Arrange - var variant = CreateVariantWithCount(EvtVariantType.StringArray, IntPtr.Zero, 0); + var testString = "Test String"; + IntPtr stringPtr = Marshal.StringToHGlobalUni(testString); - // Act - var result = NativeMethods.ConvertVariant(variant); + try + { + var variant = CreateVariant(EvtVariantType.String, stringPtr); - // Assert - Assert.NotNull(result); - Assert.IsType(result); - Assert.Empty((string[])result); + // Act + var result = NativeMethods.ConvertVariant(variant); + + // Assert + Assert.NotNull(result); + Assert.IsType(result); + Assert.Equal(testString, result); + } + finally + { + Marshal.FreeHGlobal(stringPtr); + } } [Fact] @@ -581,19 +581,18 @@ public void ConvertVariant_WhenSysTime_ShouldReturnDateTime() } [Fact] - public void ConvertVariant_WhenUInt16_ShouldReturnUInt16() + public void ConvertVariant_WhenUInt16ArrayEmpty_ShouldReturnEmptyArray() { // Arrange - ushort expectedValue = 65000; - var variant = CreateVariant(EvtVariantType.UInt16, expectedValue); + var variant = CreateVariantWithCount(EvtVariantType.UInt16Array, IntPtr.Zero, 0); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Equal(expectedValue, result); + Assert.IsType(result); + Assert.Empty((ushort[])result); } [Fact] @@ -628,34 +627,34 @@ public void ConvertVariant_WhenUInt16Array_ShouldReturnUInt16Array() } [Fact] - public void ConvertVariant_WhenUInt16ArrayEmpty_ShouldReturnEmptyArray() + public void ConvertVariant_WhenUInt16_ShouldReturnUInt16() { // Arrange - var variant = CreateVariantWithCount(EvtVariantType.UInt16Array, IntPtr.Zero, 0); + ushort expectedValue = 65000; + var variant = CreateVariant(EvtVariantType.UInt16, expectedValue); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Empty((ushort[])result); + Assert.IsType(result); + Assert.Equal(expectedValue, result); } [Fact] - public void ConvertVariant_WhenUInt32_ShouldReturnUInt32() + public void ConvertVariant_WhenUInt32ArrayEmpty_ShouldReturnEmptyArray() { // Arrange - uint expectedValue = 4000000000; - var variant = CreateVariant(EvtVariantType.UInt32, expectedValue); + var variant = CreateVariantWithCount(EvtVariantType.UInt32Array, IntPtr.Zero, 0); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Equal(expectedValue, result); + Assert.IsType(result); + Assert.Empty((uint[])result); } [Fact] @@ -690,18 +689,19 @@ public void ConvertVariant_WhenUInt32Array_ShouldReturnUInt32Array() } [Fact] - public void ConvertVariant_WhenUInt32ArrayEmpty_ShouldReturnEmptyArray() + public void ConvertVariant_WhenUInt32_ShouldReturnUInt32() { // Arrange - var variant = CreateVariantWithCount(EvtVariantType.UInt32Array, IntPtr.Zero, 0); + uint expectedValue = 4000000000; + var variant = CreateVariant(EvtVariantType.UInt32, expectedValue); // Act var result = NativeMethods.ConvertVariant(variant); // Assert Assert.NotNull(result); - Assert.IsType(result); - Assert.Empty((uint[])result); + Assert.IsType(result); + Assert.Equal(expectedValue, result); } [Fact] diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs index 79e0f32d..808e0479 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/CompressedJsonValueConverterTests.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.Eventing.Tests.TestUtils; +using System.Text; using System.Text.Json; namespace EventLogExpert.Eventing.Tests.ProviderDatabase; @@ -66,7 +67,7 @@ public void ConvertToCompressedJson_ShouldProduceSmallerOutput_ForLargeData() var largeData = CompressionTestUtils.CreateLargeTestData(); var uncompressedJson = JsonSerializer.Serialize(largeData); - var uncompressedBytes = System.Text.Encoding.UTF8.GetBytes(uncompressedJson); + var uncompressedBytes = Encoding.UTF8.GetBytes(uncompressedJson); // Act var compressed = CompressedJsonValueConverter.ConvertToCompressedJson(largeData); diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs index 9aac8bff..7e98d6db 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/ProviderDatabase/ProviderDbContextTests.cs @@ -8,6 +8,7 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using NSubstitute; +using System.Text; namespace EventLogExpert.Eventing.Tests.ProviderDatabase; @@ -763,7 +764,7 @@ public void ProviderName_LookupOnV4Schema_UsesPrimaryKeyIndex() cmd.CommandText = "EXPLAIN QUERY PLAN SELECT * FROM \"ProviderDetails\" WHERE \"ProviderName\" = 'provider-2'"; using var reader = cmd.ExecuteReader(); - var planText = new System.Text.StringBuilder(); + var planText = new StringBuilder(); while (reader.Read()) { planText.AppendLine(reader["detail"]?.ToString()); diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs index 68952262..445f233c 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/EventProviderDatabaseEventResolverTests.cs @@ -262,61 +262,61 @@ public void Dispose_MultipleConcurrentDisposeCalls_ShouldHandleThreadSafely() } [Fact] - public void Dispose_ThenResolveEventViaBaseReference_ShouldThrowObjectDisposedException() + public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException() { // Arrange var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); - EventResolver resolver = new EventResolver(dbCollection); - - // Type as base class to verify override (not 'new') is used - EventResolverBase baseResolver = resolver; + var resolver = new EventResolver(dbCollection); - var eventRecord = EventUtils.CreateBasicEvent(); + var eventRecord = new EventRecord + { + ProviderName = Constants.TestProviderName, + Id = 1000 + }; // Act resolver.Dispose(); - // Assert - This should throw because ResolveEvent is overridden, not hidden with 'new' - Assert.Throws(() => baseResolver.ResolveEvent(eventRecord)); + // Assert + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); } [Fact] - public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() + public void Dispose_ThenResolveEventViaBaseReference_ShouldThrowObjectDisposedException() { // Arrange var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); - var resolver = new EventResolver(dbCollection); + EventResolver resolver = new EventResolver(dbCollection); + + // Type as base class to verify override (not 'new') is used + EventResolverBase baseResolver = resolver; var eventRecord = EventUtils.CreateBasicEvent(); // Act resolver.Dispose(); - // Assert - Assert.Throws(() => resolver.ResolveEvent(eventRecord)); + // Assert - This should throw because ResolveEvent is overridden, not hidden with 'new' + Assert.Throws(() => baseResolver.ResolveEvent(eventRecord)); } [Fact] - public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException() + public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() { // Arrange var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns([]); var resolver = new EventResolver(dbCollection); - var eventRecord = new EventRecord - { - ProviderName = Constants.TestProviderName, - Id = 1000 - }; + var eventRecord = EventUtils.CreateBasicEvent(); // Act resolver.Dispose(); // Assert - Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); + Assert.Throws(() => resolver.ResolveEvent(eventRecord)); } [Fact] diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs index aae01abd..47c09cc1 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/LocalProviderEventResolverTests.cs @@ -109,20 +109,6 @@ public void Dispose_MultipleConcurrentDisposeCalls_ShouldHandleThreadSafely() Assert.Empty(exceptions); } - [Fact] - public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() - { - // Arrange - var resolver = new EventResolver(); - var eventRecord = EventUtils.CreateBasicEvent(); - - // Act - resolver.Dispose(); - - // Assert - Assert.Throws(() => resolver.ResolveEvent(eventRecord)); - } - [Fact] public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException() { @@ -138,88 +124,17 @@ public void Dispose_ThenLoadProviderDetails_ShouldThrowObjectDisposedException() } [Fact] - public void MultipleResolvers_WithDifferentInstances_ShouldResolveSeparately() - { - // Arrange - var resolver1 = new EventResolver(); - var resolver2 = new EventResolver(); - - var eventRecord = EventUtils.CreateBasicEvent(); - - // Act - resolver1.LoadProviderDetails(eventRecord); - resolver2.LoadProviderDetails(eventRecord); - var displayEvent1 = resolver1.ResolveEvent(eventRecord); - var displayEvent2 = resolver2.ResolveEvent(eventRecord); - - // Assert - Assert.NotNull(displayEvent1); - Assert.NotNull(displayEvent2); - Assert.Equal(displayEvent1.Source, displayEvent2.Source); - } - - [Fact] - public void MultipleResolvers_WithSharedCache_ShouldShareCachedStrings() - { - // Arrange - var sharedCache = new EventResolverCache(); - var resolver1 = new EventResolver(cache: sharedCache); - var resolver2 = new EventResolver(cache: sharedCache); - - var eventRecord = EventUtils.CreateBasicEvent(); - - // Act - resolver1.LoadProviderDetails(eventRecord); - var displayEvent1 = resolver1.ResolveEvent(eventRecord); - - resolver2.LoadProviderDetails(eventRecord); - var displayEvent2 = resolver2.ResolveEvent(eventRecord); - - // Assert - Assert.NotNull(displayEvent1); - Assert.NotNull(displayEvent2); - // With shared cache, string values should be the same reference - Assert.Same(displayEvent1.ComputerName, displayEvent2.ComputerName); - Assert.Same(displayEvent1.LogName, displayEvent2.LogName); - Assert.Same(displayEvent1.Source, displayEvent2.Source); - } - - [Fact] - public void ResolveEvent_WithMultipleProviders_ShouldResolveEachCorrectly() - { - // Arrange - var resolver = new EventResolver(); - var eventRecords = EventUtils.CreateDifferentEvents().ToList(); - - // Act - resolver.LoadProviderDetails(eventRecords[0]); - resolver.LoadProviderDetails(eventRecords[1]); - var displayEvent1 = resolver.ResolveEvent(eventRecords[0]); - var displayEvent2 = resolver.ResolveEvent(eventRecords[1]); - - // Assert - Assert.NotNull(displayEvent1); - Assert.NotNull(displayEvent2); - Assert.Equal(Constants.ApplicationLogName, displayEvent1.LogName); - Assert.Equal(Constants.SystemLogName, displayEvent2.LogName); - Assert.NotEqual(displayEvent1.ComputerName, displayEvent2.ComputerName); - } - - [Fact] - public void ResolveEvent_WithoutCallingLoadProviderDetails_ShouldStillResolve() + public void Dispose_ThenResolveEvent_ShouldThrowObjectDisposedException() { // Arrange var resolver = new EventResolver(); var eventRecord = EventUtils.CreateBasicEvent(); // Act - var displayEvent = resolver.ResolveEvent(eventRecord); + resolver.Dispose(); // Assert - Assert.NotNull(displayEvent); - Assert.Equal(eventRecord.Id, displayEvent.Id); - Assert.Equal(eventRecord.ComputerName, displayEvent.ComputerName); - Assert.NotNull(displayEvent.Description); + Assert.Throws(() => resolver.ResolveEvent(eventRecord)); } [Fact] @@ -456,4 +371,89 @@ public void LoadProviderDetails_WithNonExistentProvider_ShouldResolveWithDefault // Non-existent providers should get a default description Assert.Contains("No matching", displayEvent.Description); } + + [Fact] + public void MultipleResolvers_WithDifferentInstances_ShouldResolveSeparately() + { + // Arrange + var resolver1 = new EventResolver(); + var resolver2 = new EventResolver(); + + var eventRecord = EventUtils.CreateBasicEvent(); + + // Act + resolver1.LoadProviderDetails(eventRecord); + resolver2.LoadProviderDetails(eventRecord); + var displayEvent1 = resolver1.ResolveEvent(eventRecord); + var displayEvent2 = resolver2.ResolveEvent(eventRecord); + + // Assert + Assert.NotNull(displayEvent1); + Assert.NotNull(displayEvent2); + Assert.Equal(displayEvent1.Source, displayEvent2.Source); + } + + [Fact] + public void MultipleResolvers_WithSharedCache_ShouldShareCachedStrings() + { + // Arrange + var sharedCache = new EventResolverCache(); + var resolver1 = new EventResolver(cache: sharedCache); + var resolver2 = new EventResolver(cache: sharedCache); + + var eventRecord = EventUtils.CreateBasicEvent(); + + // Act + resolver1.LoadProviderDetails(eventRecord); + var displayEvent1 = resolver1.ResolveEvent(eventRecord); + + resolver2.LoadProviderDetails(eventRecord); + var displayEvent2 = resolver2.ResolveEvent(eventRecord); + + // Assert + Assert.NotNull(displayEvent1); + Assert.NotNull(displayEvent2); + // With shared cache, string values should be the same reference + Assert.Same(displayEvent1.ComputerName, displayEvent2.ComputerName); + Assert.Same(displayEvent1.LogName, displayEvent2.LogName); + Assert.Same(displayEvent1.Source, displayEvent2.Source); + } + + [Fact] + public void ResolveEvent_WithMultipleProviders_ShouldResolveEachCorrectly() + { + // Arrange + var resolver = new EventResolver(); + var eventRecords = EventUtils.CreateDifferentEvents().ToList(); + + // Act + resolver.LoadProviderDetails(eventRecords[0]); + resolver.LoadProviderDetails(eventRecords[1]); + var displayEvent1 = resolver.ResolveEvent(eventRecords[0]); + var displayEvent2 = resolver.ResolveEvent(eventRecords[1]); + + // Assert + Assert.NotNull(displayEvent1); + Assert.NotNull(displayEvent2); + Assert.Equal(Constants.ApplicationLogName, displayEvent1.LogName); + Assert.Equal(Constants.SystemLogName, displayEvent2.LogName); + Assert.NotEqual(displayEvent1.ComputerName, displayEvent2.ComputerName); + } + + [Fact] + public void ResolveEvent_WithoutCallingLoadProviderDetails_ShouldStillResolve() + { + // Arrange + var resolver = new EventResolver(); + var eventRecord = EventUtils.CreateBasicEvent(); + + // Act + var displayEvent = resolver.ResolveEvent(eventRecord); + + // Assert + Assert.NotNull(displayEvent); + Assert.Equal(eventRecord.Id, displayEvent.Id); + Assert.Equal(eventRecord.ComputerName, displayEvent.ComputerName); + Assert.NotNull(displayEvent.Description); + } } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs index 24a1b4bb..80919016 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/Resolvers/VersatileEventResolverTests.cs @@ -115,6 +115,65 @@ public void Dispose_CalledMultipleTimes_ShouldNotThrow() resolver.Dispose(); // Third call should not throw } + [Fact] + public void Dispose_ThenLoadProviderDetails_WithDatabaseResolver_ShouldThrowObjectDisposedException() + { + // Arrange + string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); + + try + { + using (var context = new ProviderDbContext(dbPath, false)) + { + context.SaveChanges(); + } + + var dbCollection = Substitute.For(); + dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); + + var resolver = new EventResolver(dbCollection); + var eventRecord = new EventRecord + { + ProviderName = Constants.TestProviderName, + Id = 1000 + }; + + // Act + resolver.Dispose(); + + // Assert + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); + } + finally + { + if (File.Exists(dbPath)) + { + SqliteConnection.ClearAllPools(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + File.Delete(dbPath); + } + } + } + + [Fact] + public void Dispose_ThenLoadProviderDetails_WithLocalResolver_ShouldThrowObjectDisposedException() + { + // Arrange + var resolver = new EventResolver(); // Uses local resolver + var eventRecord = new EventRecord + { + ProviderName = Constants.TestProviderName, + Id = 1000 + }; + + // Act + resolver.Dispose(); + + // Assert - Should throw even though local resolver doesn't hold resources + Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); + } + [Fact] public void Dispose_ThenResolveEvent_WithDatabaseResolver_ShouldThrowObjectDisposedException() { @@ -167,7 +226,57 @@ public void Dispose_ThenResolveEvent_WithLocalResolver_ShouldThrowObjectDisposed } [Fact] - public void Dispose_ThenLoadProviderDetails_WithDatabaseResolver_ShouldThrowObjectDisposedException() + public void LoadProviderDetails_CalledTwice_ShouldHandleCorrectly() + { + // Arrange + using var resolver = new EventResolver(); + + var eventRecord = new EventRecord + { + ProviderName = Constants.TestProviderName, + Id = 1000 + }; + + // Act + resolver.LoadProviderDetails(eventRecord); + var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); + + // Assert + Assert.Null(exception); + } + + [Fact] + public void LoadProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() + { + // Arrange + using var resolver = new EventResolver(); + var exceptions = new Exception?[50]; + + // Act + Parallel.For(0, 50, i => + { + try + { + var eventRecord = new EventRecord + { + ProviderName = $"Provider{i % 10}", + Id = (ushort)(1000 + i) + }; + + resolver.LoadProviderDetails(eventRecord); + } + catch (Exception ex) + { + exceptions[i] = ex; + } + }); + + // Assert + Assert.All(exceptions, ex => Assert.Null(ex)); + } + + [Fact] + public void LoadProviderDetails_WithDatabaseResolver_ShouldResolve() { // Arrange string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); @@ -182,18 +291,21 @@ public void Dispose_ThenLoadProviderDetails_WithDatabaseResolver_ShouldThrowObje var dbCollection = Substitute.For(); dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); - var resolver = new EventResolver(dbCollection); var eventRecord = new EventRecord { - ProviderName = Constants.TestProviderName, + ProviderName = "TestProvider", Id = 1000 }; // Act - resolver.Dispose(); + Exception? exception; + using (var resolver = new EventResolver(dbCollection)) + { + exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); + } // Assert - Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); + Assert.Null(exception); } finally { @@ -208,10 +320,11 @@ public void Dispose_ThenLoadProviderDetails_WithDatabaseResolver_ShouldThrowObje } [Fact] - public void Dispose_ThenLoadProviderDetails_WithLocalResolver_ShouldThrowObjectDisposedException() + public void LoadProviderDetails_WithLocalResolver_ShouldResolve() { // Arrange - var resolver = new EventResolver(); // Uses local resolver + using var resolver = new EventResolver(); + var eventRecord = new EventRecord { ProviderName = Constants.TestProviderName, @@ -219,10 +332,10 @@ public void Dispose_ThenLoadProviderDetails_WithLocalResolver_ShouldThrowObjectD }; // Act - resolver.Dispose(); + var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); - // Assert - Should throw even though local resolver doesn't hold resources - Assert.Throws(() => resolver.LoadProviderDetails(eventRecord)); + // Assert + Assert.Null(exception); } [Fact] @@ -479,119 +592,6 @@ public void ResolveEvent_WithProvider_ShouldReturnDisplayEventWithMatchingFields Assert.Equal(eventRecord.ComputerName, displayEvent.ComputerName); } - [Fact] - public void LoadProviderDetails_CalledTwice_ShouldHandleCorrectly() - { - // Arrange - using var resolver = new EventResolver(); - - var eventRecord = new EventRecord - { - ProviderName = Constants.TestProviderName, - Id = 1000 - }; - - // Act - resolver.LoadProviderDetails(eventRecord); - var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); - - // Assert - Assert.Null(exception); - } - - [Fact] - public void LoadProviderDetails_ConcurrentCalls_ShouldHandleThreadSafely() - { - // Arrange - using var resolver = new EventResolver(); - var exceptions = new Exception?[50]; - - // Act - Parallel.For(0, 50, i => - { - try - { - var eventRecord = new EventRecord - { - ProviderName = $"Provider{i % 10}", - Id = (ushort)(1000 + i) - }; - - resolver.LoadProviderDetails(eventRecord); - } - catch (Exception ex) - { - exceptions[i] = ex; - } - }); - - // Assert - Assert.All(exceptions, ex => Assert.Null(ex)); - } - - [Fact] - public void LoadProviderDetails_WithDatabaseResolver_ShouldResolve() - { - // Arrange - string dbPath = Path.Combine(Path.GetTempPath(), $"Test_{Guid.NewGuid()}.db"); - - try - { - using (var context = new ProviderDbContext(dbPath, false)) - { - context.SaveChanges(); - } - - var dbCollection = Substitute.For(); - dbCollection.ActiveDatabases.Returns(ImmutableList.Create(dbPath)); - - var eventRecord = new EventRecord - { - ProviderName = "TestProvider", - Id = 1000 - }; - - // Act - Exception? exception; - using (var resolver = new EventResolver(dbCollection)) - { - exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); - } - - // Assert - Assert.Null(exception); - } - finally - { - if (File.Exists(dbPath)) - { - SqliteConnection.ClearAllPools(); - GC.Collect(); - GC.WaitForPendingFinalizers(); - File.Delete(dbPath); - } - } - } - - [Fact] - public void LoadProviderDetails_WithLocalResolver_ShouldResolve() - { - // Arrange - using var resolver = new EventResolver(); - - var eventRecord = new EventRecord - { - ProviderName = Constants.TestProviderName, - Id = 1000 - }; - - // Act - var exception = Record.Exception(() => resolver.LoadProviderDetails(eventRecord)); - - // Assert - Assert.Null(exception); - } - [Fact] public void SwitchBetweenResolvers_WithDifferentInstances_ShouldWorkIndependently() { diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs b/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs index c4007a10..08819b05 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Provider.cs @@ -6,28 +6,28 @@ namespace EventLogExpert.Eventing.Tests.TestUtils.Constants; public sealed partial class Constants { public const string ApplicationLogName = "Application"; - public const string SystemLogName = "System"; - public const string SecurityLogName = "Security"; + + public const string ExchangeFormattedDescription = + "Database redundancy health check passed.\r\nDatabase copy: SERVER1\r\nRedundancy count: 4\r\nIsSuppressed: False\r\n\r\nErrors:\r\nLots of copy status text"; public const string KernelGeneralLogName = "Microsoft-Windows-Kernel-General"; - public const string PowerShellLogName = "Microsoft-Windows-PowerShell"; - public const string SecurityAuditingLogName = "Microsoft-Windows-Security-Auditing"; - public const string ServiceControlManagerLogName = "Service Control Manager"; + + public const string LocalComputer = "LocalComputer"; + + public const string Localhost = "localhost"; public const string NonExistentDatabaseFullPath = @"C:\Test\NonExistentDatabase.db"; public const string NonExistentDll = "NonExistent.dll"; public const string NonExistentDllFullPath = @"C:\Windows\System32\NonExistent.dll"; public const string NonExistentProviderName = "NonExistentProvider"; - - public const string Localhost = "localhost"; - - public const string LocalComputer = "LocalComputer"; + public const string PowerShellLogName = "Microsoft-Windows-PowerShell"; public const string RemoteComputer = "RemoteComputer"; + public const string SecurityAuditingLogName = "Microsoft-Windows-Security-Auditing"; + public const string SecurityLogName = "Security"; + public const string ServiceControlManagerLogName = "Service Control Manager"; + public const string SystemLogName = "System"; public const string TestComputer = "TestComputer"; public const string TestProviderLongName = "Microsoft-Windows-EventLogExpert"; public const string TestProviderName = "TestProvider"; - - public const string ExchangeFormattedDescription = - "Database redundancy health check passed.\r\nDatabase copy: SERVER1\r\nRedundancy count: 4\r\nIsSuppressed: False\r\n\r\nErrors:\r\nLots of copy status text"; } diff --git a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs b/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs index 63959859..7840004b 100644 --- a/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs +++ b/tests/Unit/EventLogExpert.Eventing.Tests/TestUtils/Constants/Constants.Resolver.cs @@ -6,25 +6,25 @@ namespace EventLogExpert.Eventing.Tests.TestUtils.Constants; public sealed partial class Constants { public const string EmptyTemplate = ""; - - public const string PrimaryTask = "PrimaryTask"; - public const string SupplementalTask = "SupplementalTask"; public const string ModernEventTask = "ModernEventTask"; + public const string PrimaryFallbackParameter = "PrimaryFallbackParameter"; public const string PrimaryKeyword = "PrimaryKeyword"; - public const string SupplementalKeyword = "SupplementalKeyword"; - public const string SupplementalKeyword2 = "SupplementalKeyword2"; - public const string SupplementalKeywordShouldLose = "SupplementalKeywordShouldLose"; - - public const string SupplementalDescription = "Supplemental description"; - public const string ResolvedParameterTemplate = "Resolved=%%1"; - - public const string PrimaryParameterValue = "PrimaryParameterValue"; - public const string SupplementalParameterValue = "SupplementalParameterValue"; - public const string PrimaryFallbackParameter = "PrimaryFallbackParameter"; public const string PrimaryMessageA = "Primary message A"; public const string PrimaryMessageB = "Primary message B"; + + public const string PrimaryParameterValue = "PrimaryParameterValue"; public const string PrimaryShortA = "Primary A"; public const string PrimaryShortB = "Primary B"; + + public const string PrimaryTask = "PrimaryTask"; + public const string ResolvedParameterTemplate = "Resolved=%%1"; + + public const string SupplementalDescription = "Supplemental description"; + public const string SupplementalKeyword = "SupplementalKeyword"; + public const string SupplementalKeyword2 = "SupplementalKeyword2"; + public const string SupplementalKeywordShouldLose = "SupplementalKeywordShouldLose"; + public const string SupplementalParameterValue = "SupplementalParameterValue"; + public const string SupplementalTask = "SupplementalTask"; } diff --git a/tests/Unit/EventLogExpert.UI.Tests/DatabaseStatusLabelsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Defaults/DatabaseStatusLabelsTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/DatabaseStatusLabelsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Defaults/DatabaseStatusLabelsTests.cs index 46bdcdbe..209db6b5 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/DatabaseStatusLabelsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Defaults/DatabaseStatusLabelsTests.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI; using EventLogExpert.UI.Models; -namespace EventLogExpert.UI.Tests; +namespace EventLogExpert.UI.Tests.Defaults; public sealed class DatabaseStatusLabelsTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Defaults/DateRangeDefaultsTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Defaults/DateRangeDefaultsTests.cs index cfddc172..1bf667d4 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/DateRangeDefaultsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Defaults/DateRangeDefaultsTests.cs @@ -6,7 +6,7 @@ using EventLogExpert.UI.Models; using EventLogExpert.UI.Tests.TestUtils; -namespace EventLogExpert.UI.Tests; +namespace EventLogExpert.UI.Tests.Defaults; public sealed class DateRangeDefaultsTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Extensions/FilterMethodsTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Extensions/FilterMethodsTests.cs index 976f5f6b..f4e5dd01 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/FilterMethodsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Extensions/FilterMethodsTests.cs @@ -7,7 +7,7 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using System.Collections.Immutable; -namespace EventLogExpert.UI.Tests; +namespace EventLogExpert.UI.Tests.Extensions; public sealed class FilterMethodsTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/GlobalUsings.cs b/tests/Unit/EventLogExpert.UI.Tests/GlobalUsings.cs index c802f448..b04ace3e 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/GlobalUsings.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/GlobalUsings.cs @@ -1 +1,4 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + global using Xunit; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterDataDraftTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDataDraftTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Models/FilterDataDraftTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDataDraftTests.cs index 27304eba..b3ec467a 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterDataDraftTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDataDraftTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Models; -namespace EventLogExpert.UI.Tests.Models; +namespace EventLogExpert.UI.Tests.Models.Filter; public sealed class FilterDataDraftTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterDataTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDataTests.cs similarity index 97% rename from tests/Unit/EventLogExpert.UI.Tests/Models/FilterDataTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDataTests.cs index 841a6265..622670e1 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterDataTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDataTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.UI.Models; using EventLogExpert.UI.Tests.TestUtils.Constants; -namespace EventLogExpert.UI.Tests.Models; +namespace EventLogExpert.UI.Tests.Models.Filter; public sealed class FilterDataTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterDraftModelTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDraftModelTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Models/FilterDraftModelTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDraftModelTests.cs index 8d052eb0..3a79679c 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterDraftModelTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterDraftModelTests.cs @@ -5,7 +5,7 @@ using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; -namespace EventLogExpert.UI.Tests.Models; +namespace EventLogExpert.UI.Tests.Models.Filter; public sealed class FilterDraftModelTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterModelTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterModelTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Models/FilterModelTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterModelTests.cs index b21e5e99..f69f0bc4 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Models/FilterModelTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/FilterModelTests.cs @@ -5,7 +5,7 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using System.Text.Json; -namespace EventLogExpert.UI.Tests.Models; +namespace EventLogExpert.UI.Tests.Models.Filter; public sealed class FilterModelTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Models/RequiresXmlTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/RequiresXmlTests.cs similarity index 77% rename from tests/Unit/EventLogExpert.UI.Tests/Models/RequiresXmlTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Models/Filter/RequiresXmlTests.cs index b8630649..3d974528 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Models/RequiresXmlTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Models/Filter/RequiresXmlTests.cs @@ -5,13 +5,12 @@ using EventLogExpert.UI.Tests.TestUtils; using System.Collections.Immutable; -namespace EventLogExpert.UI.Tests.Models; +namespace EventLogExpert.UI.Tests.Models.Filter; /// -/// Validates aggregation across filters. -/// This flag drives selective log reloads in EventLogEffects.HandleSetFilters; -/// false positives cause unnecessary reloads, false negatives cause silently broken filters. -/// Per-expression XML detection is covered by FilterCompilerTests. +/// Validates aggregation across filters. This flag drives selective log +/// reloads in Effects.HandleSetFilters; false positives cause unnecessary reloads, false negatives cause +/// silently broken filters. Per-expression XML detection is covered by FilterCompilerTests. /// public sealed class RequiresXmlTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Models/GitReleaseModelTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Models/Update/GitReleaseModelTests.cs similarity index 82% rename from tests/Unit/EventLogExpert.UI.Tests/Models/GitReleaseModelTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Models/Update/GitReleaseModelTests.cs index 4d5a3560..cc646756 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Models/GitReleaseModelTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Models/Update/GitReleaseModelTests.cs @@ -1,6 +1,9 @@ -using EventLogExpert.UI.Tests.TestUtils; +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. -namespace EventLogExpert.UI.Tests.Models; +using EventLogExpert.UI.Tests.TestUtils; + +namespace EventLogExpert.UI.Tests.Models.Update; public sealed class GitReleaseModelTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/EmptyLogAlertFormatterTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/EmptyLogAlertFormatterTests.cs similarity index 96% rename from tests/Unit/EventLogExpert.UI.Tests/Services/EmptyLogAlertFormatterTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/EmptyLogAlertFormatterTests.cs index 9caee4ae..023a2799 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/EmptyLogAlertFormatterTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/EmptyLogAlertFormatterTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Alerts; public sealed class EmptyLogAlertFormatterTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs new file mode 100644 index 00000000..ef11065f --- /dev/null +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs @@ -0,0 +1,161 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Services; +using NSubstitute; + +namespace EventLogExpert.UI.Tests.Services.Alerts; + +public sealed class InlineAlertHostBrokerTests +{ + [Fact] + public void Register_AfterUnregister_OverwritesPriorState() + { + // Arrange — same modal id reuses the broker after an explicit Unregister. + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(5L); + var broker = new InlineAlertHostBroker(modalService); + broker.Register(5L, new FakeInlineAlertHost { Tag = "first" }); + broker.Unregister(5L); + + var second = new FakeInlineAlertHost { Tag = "second" }; + + // Act + broker.Register(5L, second); + + // Assert + Assert.True(broker.TryGet(out var resolved)); + Assert.Same(second, resolved); + } + + [Fact] + public void Register_NullHost_Throws() + { + // Arrange + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(1L); + var broker = new InlineAlertHostBroker(modalService); + + // Act + Assert + Assert.Throws(() => broker.Register(1L, null!)); + } + + [Fact] + public void Register_WithCurrentId_ExposesHost() + { + // Arrange + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(7L); + var broker = new InlineAlertHostBroker(modalService); + var host = new FakeInlineAlertHost(); + + // Act + broker.Register(7L, host); + + // Assert + Assert.True(broker.TryGet(out var resolved)); + Assert.Same(host, resolved); + } + + [Fact] + public void Register_WithStaleId_IsNoOp() + { + // Arrange — modal A was active when registration was queued, but modal B is active by the time the call lands. + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(2L); // current = B + var broker = new InlineAlertHostBroker(modalService); + var staleHost = new FakeInlineAlertHost(); + + // Act + broker.Register(1L, staleHost); // A's late registration + + // Assert — broker does not adopt the stale host. + Assert.False(broker.TryGet(out var resolved)); + Assert.Null(resolved); + } + + [Fact] + public void TryGet_AfterActiveModalChanged_LazilyInvalidatesStaleHost() + { + // Arrange — modal A registered while active. Modal B has since replaced A but A has not yet + // unregistered (Dispose hasn't fired). The broker must not surface A's host to alert dispatchers. + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(1L); + var broker = new InlineAlertHostBroker(modalService); + broker.Register(1L, new FakeInlineAlertHost()); + Assert.True(broker.TryGet(out _)); + + modalService.ActiveModalId.Returns(2L); // B replaced A. + + // Act + var found = broker.TryGet(out var resolved); + + // Assert + Assert.False(found); + Assert.Null(resolved); + } + + [Fact] + public void TryGet_WhenNoHostRegistered_ReturnsFalse() + { + // Arrange + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(0L); + var broker = new InlineAlertHostBroker(modalService); + + // Act + var found = broker.TryGet(out var resolved); + + // Assert + Assert.False(found); + Assert.Null(resolved); + } + + [Fact] + public void Unregister_WithCurrentId_ClearsHost() + { + // Arrange + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(3L); + var broker = new InlineAlertHostBroker(modalService); + broker.Register(3L, new FakeInlineAlertHost()); + + // Act + broker.Unregister(3L); + + // Assert + Assert.False(broker.TryGet(out var resolved)); + Assert.Null(resolved); + } + + [Fact] + public void Unregister_WithStaleId_DoesNotClearCurrentHost() + { + // Arrange — A registered, then B replaced A and registered. A's late Dispose fires Unregister(A.id). + // B's host must remain intact. + var modalService = Substitute.For(); + modalService.ActiveModalId.Returns(10L); + var broker = new InlineAlertHostBroker(modalService); + broker.Register(10L, new FakeInlineAlertHost { Tag = "A" }); + + modalService.ActiveModalId.Returns(11L); + var bHost = new FakeInlineAlertHost { Tag = "B" }; + broker.Register(11L, bHost); + + // Act — A's stale Unregister. + broker.Unregister(10L); + + // Assert + Assert.True(broker.TryGet(out var resolved)); + Assert.Same(bHost, resolved); + } + + private sealed class FakeInlineAlertHost : IInlineAlertHost + { + public string? Tag { get; set; } + + public Task ShowInlineAlertAsync(InlineAlertRequest request, CancellationToken cancellationToken) => + Task.FromResult(new InlineAlertResult(true, null)); + } +} diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/ModalAlertDialogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs similarity index 84% rename from tests/Unit/EventLogExpert.UI.Tests/Services/ModalAlertDialogServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs index db1c693e..b43a24a6 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/ModalAlertDialogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs @@ -6,7 +6,7 @@ using EventLogExpert.UI.Services; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Alerts; public sealed class ModalAlertDialogServiceTests { @@ -18,15 +18,15 @@ public async Task DisplayPrompt_WhenActiveHost_ShouldRouteInlineAndReturnTypedVa host.ShowInlineAlertAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new InlineAlertResult(true, "typed-value"))); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; }); var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -50,15 +50,15 @@ public async Task DisplayPrompt_WhenInlineCancelled_ShouldReturnEmptyString() host.ShowInlineAlertAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new InlineAlertResult(false, null))); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; }); var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -75,12 +75,12 @@ public async Task DisplayPrompt_WhenInlineCancelled_ShouldReturnEmptyString() public async Task DisplayPrompt_WhenNoActiveHost_ShouldCallStandalonePromptOpener() { // Arrange - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(false); + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(false); IReadOnlyDictionary? capturedPrompt = null; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -97,31 +97,6 @@ public async Task DisplayPrompt_WhenNoActiveHost_ShouldCallStandalonePromptOpene Assert.Equal("old-value", capturedPrompt["InitialValue"]); } - [Fact] - public async Task ShowAlert_ShouldMarshalThroughMainThreadService() - { - // Arrange — capture that MainThread invocation happens before the routing decision runs. - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(false); - - var mainThread = Substitute.For(); - mainThread.InvokeOnMainThreadAsync(Arg.Any>()) - .Returns(call => ((Func)call[0])()); - - var sut = new ModalAlertDialogService( - modalService, - mainThread, - Substitute.For(), - _ => Task.FromResult(true), - _ => Task.FromResult(string.Empty)); - - // Act - await sut.ShowAlert("t", "m", "c"); - - // Assert - await mainThread.Received(1).InvokeOnMainThreadAsync(Arg.Any>()); - } - [Fact] public async Task ShowAlertOneButton_BannerPresentation_DoesNotMarshalThroughMainThreadService() { @@ -130,7 +105,7 @@ public async Task ShowAlertOneButton_BannerPresentation_DoesNotMarshalThroughMai var mainThread = Substitute.For(); var sut = new ModalAlertDialogService( - Substitute.For(), + Substitute.For(), mainThread, bannerService, _ => Task.FromResult(false), @@ -149,11 +124,11 @@ public async Task ShowAlertOneButton_BannerPresentation_RoutesToReportInfoBanner { // Arrange var bannerService = Substitute.For(); - var modalService = Substitute.For(); + var broker = Substitute.For(); var standaloneCalled = false; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), bannerService, _ => { standaloneCalled = true; return Task.FromResult(false); }, @@ -165,18 +140,18 @@ public async Task ShowAlertOneButton_BannerPresentation_RoutesToReportInfoBanner // Assert bannerService.Received(1).ReportInfoBanner("Banner Title", "Banner Message", BannerSeverity.Warning); Assert.False(standaloneCalled); - modalService.DidNotReceive().TryGetActiveAlertHost(out Arg.Any()); + broker.DidNotReceive().TryGet(out Arg.Any()); } [Fact] public async Task ShowAlertOneButton_InlineOnlyNoHost_ThrowsInvalidOperationException() { // Arrange - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(false); + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(false); var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -195,8 +170,8 @@ public async Task ShowAlertOneButton_InlineOnlyWithHost_RoutesInline() host.ShowInlineAlertAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new InlineAlertResult(true, null))); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; @@ -204,7 +179,7 @@ public async Task ShowAlertOneButton_InlineOnlyWithHost_RoutesInline() var standaloneCalled = false; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => { standaloneCalled = true; return Task.FromResult(false); }, @@ -223,8 +198,8 @@ public async Task ShowAlertOneButton_PopupOnly_AlwaysOpensStandalone_EvenWithHos { // Arrange var host = Substitute.For(); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; @@ -232,7 +207,7 @@ public async Task ShowAlertOneButton_PopupOnly_AlwaysOpensStandalone_EvenWithHos IReadOnlyDictionary? capturedAlert = null; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), parameters => { capturedAlert = parameters; return Task.FromResult(true); }, @@ -251,12 +226,12 @@ public async Task ShowAlertOneButton_PopupOnly_AlwaysOpensStandalone_EvenWithHos public async Task ShowAlertOneButton_WhenNoActiveHost_ShouldCallStandaloneOpener() { // Arrange - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(false); + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(false); IReadOnlyDictionary? capturedAlert = null; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), parameters => { capturedAlert = parameters; return Task.FromResult(true); }, @@ -278,7 +253,7 @@ public async Task ShowAlertTwoButton_BannerPresentation_ThrowsArgumentException( { // Arrange var sut = new ModalAlertDialogService( - Substitute.For(), + Substitute.For(), PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -294,11 +269,11 @@ public async Task ShowAlertTwoButton_BannerPresentation_ThrowsArgumentException( public async Task ShowAlertTwoButton_InlineOnlyNoHost_ThrowsInvalidOperationException() { // Arrange - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(false); + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(false); var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -314,8 +289,8 @@ public async Task ShowAlertTwoButton_PopupOnly_AlwaysOpensStandalone() { // Arrange var host = Substitute.For(); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; @@ -323,7 +298,7 @@ public async Task ShowAlertTwoButton_PopupOnly_AlwaysOpensStandalone() IReadOnlyDictionary? capturedAlert = null; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), parameters => { capturedAlert = parameters; return Task.FromResult(true); }, @@ -348,8 +323,8 @@ public async Task ShowAlertTwoButton_WhenActiveHost_ShouldRouteToHostInline() host.ShowInlineAlertAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new InlineAlertResult(true, null))); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; @@ -357,7 +332,7 @@ public async Task ShowAlertTwoButton_WhenActiveHost_ShouldRouteToHostInline() var standaloneCalled = false; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => { standaloneCalled = true; return Task.FromResult(false); }, @@ -387,15 +362,15 @@ public async Task ShowAlertTwoButton_WhenInlineCancelled_ShouldReturnFalse() host.ShowInlineAlertAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromException(new TaskCanceledException())); - var modalService = Substitute.For(); - modalService.TryGetActiveAlertHost(out Arg.Any()).Returns(call => + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(call => { call[0] = host; return true; }); var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), Substitute.For(), _ => Task.FromResult(false), @@ -408,6 +383,31 @@ public async Task ShowAlertTwoButton_WhenInlineCancelled_ShouldReturnFalse() Assert.False(result); } + [Fact] + public async Task ShowAlert_ShouldMarshalThroughMainThreadService() + { + // Arrange — capture that MainThread invocation happens before the routing decision runs. + var broker = Substitute.For(); + broker.TryGet(out Arg.Any()).Returns(false); + + var mainThread = Substitute.For(); + mainThread.InvokeOnMainThreadAsync(Arg.Any>()) + .Returns(call => ((Func)call[0])()); + + var sut = new ModalAlertDialogService( + broker, + mainThread, + Substitute.For(), + _ => Task.FromResult(true), + _ => Task.FromResult(string.Empty)); + + // Act + await sut.ShowAlert("t", "m", "c"); + + // Assert + await mainThread.Received(1).InvokeOnMainThreadAsync(Arg.Any>()); + } + [Fact] public async Task ShowErrorAlert_DoesNotMarshalThroughMainThreadService() { @@ -416,7 +416,7 @@ public async Task ShowErrorAlert_DoesNotMarshalThroughMainThreadService() var mainThread = Substitute.For(); var sut = new ModalAlertDialogService( - Substitute.For(), + Substitute.For(), mainThread, bannerService, _ => Task.FromResult(false), @@ -435,11 +435,11 @@ public async Task ShowErrorAlert_RoutesToBannerServiceReportError_WithTitleAndMe { // Arrange var bannerService = Substitute.For(); - var modalService = Substitute.For(); + var broker = Substitute.For(); var standaloneCalled = false; var sut = new ModalAlertDialogService( - modalService, + broker, PassthroughMainThread(), bannerService, _ => { standaloneCalled = true; return Task.FromResult(false); }, @@ -451,7 +451,7 @@ public async Task ShowErrorAlert_RoutesToBannerServiceReportError_WithTitleAndMe // Assert bannerService.Received(1).ReportError("Error Title", "Error Message"); Assert.False(standaloneCalled); - modalService.DidNotReceive().TryGetActiveAlertHost(out Arg.Any()); + broker.DidNotReceive().TryGet(out Arg.Any()); } [Fact] @@ -462,7 +462,7 @@ public async Task ShowErrorAlert_WithActionLabelAndAction_PassesThroughToBannerS Func action = () => Task.CompletedTask; var sut = new ModalAlertDialogService( - Substitute.For(), + Substitute.For(), PassthroughMainThread(), bannerService, _ => Task.FromResult(false), @@ -475,6 +475,16 @@ public async Task ShowErrorAlert_WithActionLabelAndAction_PassesThroughToBannerS bannerService.Received(1).ReportError("Error Title", "Error Message", "Resolve", action); } - private static IMainThreadService PassthroughMainThread() => - new MainThreadService(action => { action(); return Task.CompletedTask; }); + private static IMainThreadService PassthroughMainThread() => new PassthroughMainThreadService(); + + private sealed class PassthroughMainThreadService : IMainThreadService + { + public Task InvokeOnMainThread(Action action) + { + action(); + return Task.CompletedTask; + } + + public Task InvokeOnMainThreadAsync(Func action) => action(); + } } diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerServiceTests.cs index 54ff4843..d0a65546 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/BannerServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerServiceTests.cs @@ -7,7 +7,7 @@ using EventLogExpert.UI.Services; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Banner; public sealed class BannerServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/BannerViewSelectorTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerViewSelectorTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/BannerViewSelectorTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerViewSelectorTests.cs index fbb94443..64ab4b72 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/BannerViewSelectorTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerViewSelectorTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Banner; public sealed class BannerViewSelectorTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs index 32b88659..038de850 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DatabaseServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs @@ -12,7 +12,7 @@ using NSubstitute; using System.IO.Compression; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Database; public sealed class DatabaseServiceTests : IDisposable { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogEntryParserTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogEntryParserTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogEntryParserTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogEntryParserTests.cs index 703cd782..04164690 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogEntryParserTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogEntryParserTests.cs @@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging; using System.Globalization; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.DebugLog; public sealed class DebugLogEntryParserTests { @@ -206,30 +206,6 @@ public void Parse_WhenMultipleContinuationLines_ShouldFoldAllIntoPrevious() Assert.Equal($"{firstLine}\n{string.Join('\n', stackTrace)}", entry.RawLine); } - [Fact] - public void Parse_WhenMultipleEntries_ShouldReturnInOrder() - { - // Arrange - string[] lines = - [ - BuildLine(Constants.DebugLogTestTimestamp, Constants.DebugLogTestThreadId, nameof(LogLevel.Trace), Constants.DebugLogFirstMessage), - BuildLine(Constants.DebugLogTestTimestamp, Constants.DebugLogTestThreadId, nameof(LogLevel.Information), Constants.DebugLogSecondMessage), - BuildLine(Constants.DebugLogTestTimestamp, Constants.DebugLogTestThreadId, nameof(LogLevel.Error), Constants.DebugLogThirdMessage) - ]; - - // Act - var entries = DebugLogEntryParser.Parse(lines); - - // Assert - Assert.Equal(3, entries.Count); - Assert.Equal(LogLevel.Trace, entries[0].Level); - Assert.Equal(Constants.DebugLogFirstMessage, entries[0].Message); - Assert.Equal(LogLevel.Information, entries[1].Level); - Assert.Equal(Constants.DebugLogSecondMessage, entries[1].Message); - Assert.Equal(LogLevel.Error, entries[2].Level); - Assert.Equal(Constants.DebugLogThirdMessage, entries[2].Message); - } - [Fact] public void Parse_WhenMultipleEntriesEachWithContinuations_ShouldFoldContinuationsIntoTheirRespectiveEntry() { @@ -269,6 +245,30 @@ public void Parse_WhenMultipleEntriesEachWithContinuations_ShouldFoldContinuatio entries[1].RawLine); } + [Fact] + public void Parse_WhenMultipleEntries_ShouldReturnInOrder() + { + // Arrange + string[] lines = + [ + BuildLine(Constants.DebugLogTestTimestamp, Constants.DebugLogTestThreadId, nameof(LogLevel.Trace), Constants.DebugLogFirstMessage), + BuildLine(Constants.DebugLogTestTimestamp, Constants.DebugLogTestThreadId, nameof(LogLevel.Information), Constants.DebugLogSecondMessage), + BuildLine(Constants.DebugLogTestTimestamp, Constants.DebugLogTestThreadId, nameof(LogLevel.Error), Constants.DebugLogThirdMessage) + ]; + + // Act + var entries = DebugLogEntryParser.Parse(lines); + + // Assert + Assert.Equal(3, entries.Count); + Assert.Equal(LogLevel.Trace, entries[0].Level); + Assert.Equal(Constants.DebugLogFirstMessage, entries[0].Message); + Assert.Equal(LogLevel.Information, entries[1].Level); + Assert.Equal(Constants.DebugLogSecondMessage, entries[1].Message); + Assert.Equal(LogLevel.Error, entries[2].Level); + Assert.Equal(Constants.DebugLogThirdMessage, entries[2].Message); + } + [Fact] public void Parse_WhenOrphanFollowedByContinuationLines_ShouldFoldContinuationsIntoOrphan() { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogProjectionTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogProjectionTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogProjectionTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogProjectionTests.cs index e36778b8..5f28d795 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogProjectionTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogProjectionTests.cs @@ -6,10 +6,114 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.DebugLog; public sealed class DebugLogProjectionTests { + [Fact] + public void ProjectRange_WhenFilterApplied_ShouldEvaluateAgainstSliceOnly() + { + // Arrange + var entries = new[] + { + BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), + BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), + BuildEntry(LogLevel.Information, Constants.DebugLogThirdMessage), + }; + + // Act + var (lines, count) = DebugLogProjection.ProjectRange( + entries, + startIndex: 1, + endIndex: 3, + FilterEvaluator.Equals, + [LogLevel.Information], + string.Empty); + + // Assert + Assert.Equal(1, count); + var only = Assert.Single(lines); + Assert.Contains(Constants.DebugLogThirdMessage, only); + } + + [Theory] + [InlineData(-1, 0)] + [InlineData(0, 4)] + [InlineData(2, 1)] + public void ProjectRange_WhenIndicesOutOfRange_ShouldThrow(int startIndex, int endIndex) + { + // Arrange + var entries = new[] + { + BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), + BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), + BuildEntry(LogLevel.Information, Constants.DebugLogThirdMessage), + }; + + // Act + Assert + Assert.Throws(() => + DebugLogProjection.ProjectRange( + entries, + startIndex, + endIndex, + FilterEvaluator.Equals, + [], + string.Empty)); + } + + [Fact] + public void ProjectRange_WhenSliceIsEmpty_ShouldReturnEmpty() + { + // Arrange + var entries = new[] + { + BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), + BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), + }; + + // Act + var (lines, count) = DebugLogProjection.ProjectRange( + entries, + startIndex: 2, + endIndex: 2, + FilterEvaluator.Equals, + [], + string.Empty); + + // Assert + Assert.Empty(lines); + Assert.Equal(0, count); + } + + [Fact] + public void ProjectRange_WhenSliceIsTrailingThird_ShouldReturnOnlyThatSliceInDisplayOrderViaReversedView() + { + // Arrange + var entries = new[] + { + BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), + BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), + BuildEntry(LogLevel.Error, Constants.DebugLogThirdMessage), + }; + + // Act + var (lines, count) = DebugLogProjection.ProjectRange( + entries, + startIndex: 1, + endIndex: 3, + FilterEvaluator.Equals, + [], + string.Empty); + + var view = new ReversedListView(lines); + + // Assert + Assert.Equal(2, count); + Assert.Equal(2, view.Count); + Assert.Contains(Constants.DebugLogThirdMessage, view[0]); + Assert.Contains(Constants.DebugLogSecondMessage, view[1]); + } + [Fact] public void Project_WhenEntriesNull_ShouldThrow() { @@ -100,61 +204,60 @@ public void Project_WhenLevelFilterActiveAndEntryHasNullLevel_ShouldExcludeEntry } [Fact] - public void Project_WhenLevelMultiSelect_ShouldKeepAnyListedLevel() + public void Project_WhenLevelMultiSelectAndEntryHasNullLevel_ShouldExcludeEntry() { // Arrange var entries = new[] { BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), - BuildEntry(LogLevel.Error, Constants.DebugLogThirdMessage), - BuildEntry(LogLevel.Critical, Constants.DebugLogNewMessage), + new DebugLogEntry(null, null, null, 0, "orphan"), }; // Act - var (view, count) = ProjectView( + var (_, count) = DebugLogProjection.Project( entries, FilterEvaluator.MultiSelect, - [LogLevel.Error, LogLevel.Critical], + [LogLevel.Information, LogLevel.Warning], string.Empty); // Assert - Assert.Equal(2, count); - Assert.Equal(2, view.Count); - Assert.Contains(Constants.DebugLogNewMessage, view[0]); - Assert.Contains(Constants.DebugLogThirdMessage, view[1]); + Assert.Equal(1, count); } [Fact] - public void Project_WhenLevelMultiSelectAndEntryHasNullLevel_ShouldExcludeEntry() + public void Project_WhenLevelMultiSelect_ShouldKeepAnyListedLevel() { // Arrange var entries = new[] { BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - new DebugLogEntry(null, null, null, 0, "orphan"), + BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), + BuildEntry(LogLevel.Error, Constants.DebugLogThirdMessage), + BuildEntry(LogLevel.Critical, Constants.DebugLogNewMessage), }; // Act - var (_, count) = DebugLogProjection.Project( + var (view, count) = ProjectView( entries, FilterEvaluator.MultiSelect, - [LogLevel.Information, LogLevel.Warning], + [LogLevel.Error, LogLevel.Critical], string.Empty); // Assert - Assert.Equal(1, count); + Assert.Equal(2, count); + Assert.Equal(2, view.Count); + Assert.Contains(Constants.DebugLogNewMessage, view[0]); + Assert.Contains(Constants.DebugLogThirdMessage, view[1]); } [Fact] - public void Project_WhenLevelNotEqual_ShouldExcludeMatchingLevel() + public void Project_WhenLevelNotEqualAndEntryHasNullLevel_ShouldIncludeEntry() { - // Arrange + // Arrange — null Level is "not equal" to any specific level, so NotEqual must include it. var entries = new[] { BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), - BuildEntry(LogLevel.Information, Constants.DebugLogThirdMessage), + new DebugLogEntry(null, null, null, 0, "orphan"), }; // Act @@ -167,17 +270,18 @@ public void Project_WhenLevelNotEqual_ShouldExcludeMatchingLevel() // Assert Assert.Equal(1, count); var only = Assert.Single(lines); - Assert.Contains(Constants.DebugLogSecondMessage, only); + Assert.Equal("orphan", only); } [Fact] - public void Project_WhenLevelNotEqualAndEntryHasNullLevel_ShouldIncludeEntry() + public void Project_WhenLevelNotEqual_ShouldExcludeMatchingLevel() { - // Arrange — null Level is "not equal" to any specific level, so NotEqual must include it. + // Arrange var entries = new[] { BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - new DebugLogEntry(null, null, null, 0, "orphan"), + BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), + BuildEntry(LogLevel.Information, Constants.DebugLogThirdMessage), }; // Act @@ -190,7 +294,7 @@ public void Project_WhenLevelNotEqualAndEntryHasNullLevel_ShouldIncludeEntry() // Assert Assert.Equal(1, count); var only = Assert.Single(lines); - Assert.Equal("orphan", only); + Assert.Contains(Constants.DebugLogSecondMessage, only); } [Fact] @@ -331,110 +435,6 @@ public void Project_WhenTextFilterMatches_ShouldKeepOnlyContainingEntries() Assert.Contains("alpha foo bravo", view[1]); } - [Fact] - public void ProjectRange_WhenFilterApplied_ShouldEvaluateAgainstSliceOnly() - { - // Arrange - var entries = new[] - { - BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), - BuildEntry(LogLevel.Information, Constants.DebugLogThirdMessage), - }; - - // Act - var (lines, count) = DebugLogProjection.ProjectRange( - entries, - startIndex: 1, - endIndex: 3, - FilterEvaluator.Equals, - [LogLevel.Information], - string.Empty); - - // Assert - Assert.Equal(1, count); - var only = Assert.Single(lines); - Assert.Contains(Constants.DebugLogThirdMessage, only); - } - - [Theory] - [InlineData(-1, 0)] - [InlineData(0, 4)] - [InlineData(2, 1)] - public void ProjectRange_WhenIndicesOutOfRange_ShouldThrow(int startIndex, int endIndex) - { - // Arrange - var entries = new[] - { - BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), - BuildEntry(LogLevel.Information, Constants.DebugLogThirdMessage), - }; - - // Act + Assert - Assert.Throws(() => - DebugLogProjection.ProjectRange( - entries, - startIndex, - endIndex, - FilterEvaluator.Equals, - [], - string.Empty)); - } - - [Fact] - public void ProjectRange_WhenSliceIsEmpty_ShouldReturnEmpty() - { - // Arrange - var entries = new[] - { - BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), - }; - - // Act - var (lines, count) = DebugLogProjection.ProjectRange( - entries, - startIndex: 2, - endIndex: 2, - FilterEvaluator.Equals, - [], - string.Empty); - - // Assert - Assert.Empty(lines); - Assert.Equal(0, count); - } - - [Fact] - public void ProjectRange_WhenSliceIsTrailingThird_ShouldReturnOnlyThatSliceInDisplayOrderViaReversedView() - { - // Arrange - var entries = new[] - { - BuildEntry(LogLevel.Information, Constants.DebugLogFirstMessage), - BuildEntry(LogLevel.Warning, Constants.DebugLogSecondMessage), - BuildEntry(LogLevel.Error, Constants.DebugLogThirdMessage), - }; - - // Act - var (lines, count) = DebugLogProjection.ProjectRange( - entries, - startIndex: 1, - endIndex: 3, - FilterEvaluator.Equals, - [], - string.Empty); - - var view = new ReversedListView(lines); - - // Assert - Assert.Equal(2, count); - Assert.Equal(2, view.Count); - Assert.Contains(Constants.DebugLogThirdMessage, view[0]); - Assert.Contains(Constants.DebugLogSecondMessage, view[1]); - } - private static DebugLogEntry BuildEntry(LogLevel level, string message) { var rawLine = $"[{Constants.DebugLogTestTimestamp}] [{Constants.DebugLogTestThreadId}] [{level}] {message}"; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogServiceTests.cs index e64db784..551d1fbf 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogServiceTests.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.DebugLog; public sealed class DebugLogServiceTests : IDisposable { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs index fefecd8c..21fbca85 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DeploymentServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs @@ -10,7 +10,7 @@ using NSubstitute; using Windows.Foundation; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Deployment; public sealed class DeploymentServiceTests { @@ -192,7 +192,7 @@ public async Task RestartNowAndUpdate_WhenDeploymentCompleted_ShouldSetRelaunchM } [Fact] - public async Task RestartNowAndUpdate_WhenDeploymentFails_ShouldShowAlertAndClearProgress() + public async Task RestartNowAndUpdate_WhenDeploymentFailsAutoScan_ShouldNotShowAlertButClearProgress() { // Arrange var testException = new Exception("Test deployment error"); @@ -226,21 +226,20 @@ public async Task RestartNowAndUpdate_WhenDeploymentFails_ShouldShowAlertAndClea packageDeploymentService: mockPackageDeploymentService); // Act - deploymentService.RestartNowAndUpdate(Constants.DownloadPath, true); + deploymentService.RestartNowAndUpdate(Constants.DownloadPath, userInitiated: false); mockDeploymentOperation.SimulateCompleted(AsyncStatus.Error, testException); // Assert - await mockMainThreadService.Received(1).InvokeOnMainThreadAsync(Arg.Any>()); - await mockAlertDialogService.Received(1).ShowAlert( - Constants.UpdateFailureTitle, - Arg.Is(msg => msg.Contains(testException.ToString())), - Constants.UpdateFailureOk); + await mockAlertDialogService.DidNotReceive().ShowAlert( + Arg.Any(), + Arg.Any(), + Arg.Any()); mockAppTitleService.Received(1).SetProgressString(null); } [Fact] - public async Task RestartNowAndUpdate_WhenDeploymentFailsAutoScan_ShouldNotShowAlertButClearProgress() + public async Task RestartNowAndUpdate_WhenDeploymentFails_ShouldShowAlertAndClearProgress() { // Arrange var testException = new Exception("Test deployment error"); @@ -274,14 +273,15 @@ public async Task RestartNowAndUpdate_WhenDeploymentFailsAutoScan_ShouldNotShowA packageDeploymentService: mockPackageDeploymentService); // Act - deploymentService.RestartNowAndUpdate(Constants.DownloadPath, userInitiated: false); + deploymentService.RestartNowAndUpdate(Constants.DownloadPath, true); mockDeploymentOperation.SimulateCompleted(AsyncStatus.Error, testException); // Assert - await mockAlertDialogService.DidNotReceive().ShowAlert( - Arg.Any(), - Arg.Any(), - Arg.Any()); + await mockMainThreadService.Received(1).InvokeOnMainThreadAsync(Arg.Any>()); + await mockAlertDialogService.Received(1).ShowAlert( + Constants.UpdateFailureTitle, + Arg.Is(msg => msg.Contains(testException.ToString())), + Constants.UpdateFailureOk); mockAppTitleService.Received(1).SetProgressString(null); } @@ -438,7 +438,7 @@ public async Task UpdateOnNextRestart_WhenDeploymentCompleted_ShouldSetRelaunchM } [Fact] - public async Task UpdateOnNextRestart_WhenDeploymentFails_ShouldShowAlertAndClearProgress() + public async Task UpdateOnNextRestart_WhenDeploymentFailsAutoScan_ShouldNotShowAlertButClearProgress() { // Arrange var testException = new Exception("Test deployment error"); @@ -468,21 +468,20 @@ public async Task UpdateOnNextRestart_WhenDeploymentFails_ShouldShowAlertAndClea packageDeploymentService: mockPackageDeploymentService); // Act - deploymentService.UpdateOnNextRestart(Constants.DownloadPath, true); + deploymentService.UpdateOnNextRestart(Constants.DownloadPath, userInitiated: false); mockDeploymentOperation.SimulateCompleted(AsyncStatus.Error, testException); // Assert - await mockMainThreadService.Received(1).InvokeOnMainThreadAsync(Arg.Any>()); - await mockAlertDialogService.Received(1).ShowAlert( - Constants.UpdateFailureTitle, - Arg.Is(msg => msg.Contains(testException.ToString())), - Constants.UpdateFailureOk); + await mockAlertDialogService.DidNotReceive().ShowAlert( + Arg.Any(), + Arg.Any(), + Arg.Any()); mockAppTitleService.Received(1).SetProgressString(null); } [Fact] - public async Task UpdateOnNextRestart_WhenDeploymentFailsAutoScan_ShouldNotShowAlertButClearProgress() + public async Task UpdateOnNextRestart_WhenDeploymentFails_ShouldShowAlertAndClearProgress() { // Arrange var testException = new Exception("Test deployment error"); @@ -512,14 +511,15 @@ public async Task UpdateOnNextRestart_WhenDeploymentFailsAutoScan_ShouldNotShowA packageDeploymentService: mockPackageDeploymentService); // Act - deploymentService.UpdateOnNextRestart(Constants.DownloadPath, userInitiated: false); + deploymentService.UpdateOnNextRestart(Constants.DownloadPath, true); mockDeploymentOperation.SimulateCompleted(AsyncStatus.Error, testException); // Assert - await mockAlertDialogService.DidNotReceive().ShowAlert( - Arg.Any(), - Arg.Any(), - Arg.Any()); + await mockMainThreadService.Received(1).InvokeOnMainThreadAsync(Arg.Any>()); + await mockAlertDialogService.Received(1).ShowAlert( + Constants.UpdateFailureTitle, + Arg.Is(msg => msg.Contains(testException.ToString())), + Constants.UpdateFailureOk); mockAppTitleService.Received(1).SetProgressString(null); } diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/ReversedListViewTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Display/ReversedListViewTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/ReversedListViewTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Display/ReversedListViewTests.cs index 1cd02e33..6d9b725d 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/ReversedListViewTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Display/ReversedListViewTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Display; public sealed class ReversedListViewTests { @@ -110,49 +110,49 @@ public void Enumerate_ShouldYieldInReverseOrder() } [Fact] - public void Indexer_ShouldReturnInnerInReverseOrder() + public void IndexOf_ShouldReturnPositionInReversedView() { // Arrange - var inner = new List { "a", "b", "c" }; - var view = new ReversedListView(inner); + var view = new ReversedListView(new List { "a", "b", "c" }); - // Assert - Assert.Equal("c", view[0]); - Assert.Equal("b", view[1]); - Assert.Equal("a", view[2]); + // Act + Assert + Assert.Equal(0, view.IndexOf("c")); + Assert.Equal(1, view.IndexOf("b")); + Assert.Equal(2, view.IndexOf("a")); + Assert.Equal(-1, view.IndexOf("d")); } [Fact] - public void IndexerSetter_ShouldThrow() + public void IndexOf_WhenItemAppearsTwice_ShouldReturnFirstOccurrenceInReversedView() { - // Arrange - var view = new ReversedListView(new List { "a" }); + // Arrange — inner: [a, b, a]; reversed view: [a, b, a]; first occurrence of "a" in reversed view is index 0. + var view = new ReversedListView(new List { "a", "b", "a" }); // Act + Assert - Assert.Throws(() => ((IList)view)[0] = "z"); + Assert.Equal(0, view.IndexOf("a")); } [Fact] - public void IndexOf_ShouldReturnPositionInReversedView() + public void IndexerSetter_ShouldThrow() { // Arrange - var view = new ReversedListView(new List { "a", "b", "c" }); + var view = new ReversedListView(new List { "a" }); // Act + Assert - Assert.Equal(0, view.IndexOf("c")); - Assert.Equal(1, view.IndexOf("b")); - Assert.Equal(2, view.IndexOf("a")); - Assert.Equal(-1, view.IndexOf("d")); + Assert.Throws(() => ((IList)view)[0] = "z"); } [Fact] - public void IndexOf_WhenItemAppearsTwice_ShouldReturnFirstOccurrenceInReversedView() + public void Indexer_ShouldReturnInnerInReverseOrder() { - // Arrange — inner: [a, b, a]; reversed view: [a, b, a]; first occurrence of "a" in reversed view is index 0. - var view = new ReversedListView(new List { "a", "b", "a" }); + // Arrange + var inner = new List { "a", "b", "c" }; + var view = new ReversedListView(inner); - // Act + Assert - Assert.Equal(0, view.IndexOf("a")); + // Assert + Assert.Equal("c", view[0]); + Assert.Equal("b", view[1]); + Assert.Equal("a", view[2]); } [Fact] @@ -186,22 +186,22 @@ public void IsReadOnly_ShouldBeTrue() } [Fact] - public void Remove_ShouldThrow() + public void RemoveAt_ShouldThrow() { // Arrange var view = new ReversedListView(new List { "a" }); // Act + Assert - Assert.Throws(() => view.Remove("a")); + Assert.Throws(() => view.RemoveAt(0)); } [Fact] - public void RemoveAt_ShouldThrow() + public void Remove_ShouldThrow() { // Arrange var view = new ReversedListView(new List { "a" }); // Act + Assert - Assert.Throws(() => view.RemoveAt(0)); + Assert.Throws(() => view.Remove("a")); } } diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterCategoryItemsCacheTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterCategoryItemsCacheTests.cs index 6abbb0d0..3638e912 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCategoryItemsCacheTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterCategoryItemsCacheTests.cs @@ -9,7 +9,7 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using System.Collections.Immutable; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Filter; public sealed class FilterCategoryItemsCacheTests : IDisposable { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCompilerTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterCompilerTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/FilterCompilerTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterCompilerTests.cs index 8cb26849..c99fc5be 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterCompilerTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterCompilerTests.cs @@ -5,7 +5,7 @@ using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Filter; public sealed class FilterCompilerTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterServiceTests.cs index 741af4be..e3011ade 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/FilterServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Filter/FilterServiceTests.cs @@ -9,7 +9,7 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using System.Collections.Immutable; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Filter; public sealed class FilterServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/MenuServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Menu/MenuServiceTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/MenuServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Menu/MenuServiceTests.cs index b03f0de7..af1d02dc 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/MenuServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Menu/MenuServiceTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Menu; public sealed class MenuServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/ModalServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Modal/ModalServiceTests.cs similarity index 66% rename from tests/Unit/EventLogExpert.UI.Tests/Services/ModalServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Modal/ModalServiceTests.cs index f9e26df9..67305ef2 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/ModalServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Modal/ModalServiceTests.cs @@ -1,30 +1,13 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using Microsoft.AspNetCore.Components; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Modal; public sealed class ModalServiceTests { - [Fact] - public void CancelActive_ShouldAlsoClearAlertHost() - { - // Arrange - var service = new ModalService(); - _ = service.Show(); - service.RegisterActiveAlertHost(service.ActiveModalId, new FakeInlineAlertHost()); - - // Act - service.CancelActive(); - - // Assert - Assert.False(service.TryGetActiveAlertHost(out var resolved)); - Assert.Null(resolved); - } - [Fact] public async Task CancelActive_WhenModalIsOpen_ShouldCompleteWithDefaultAndClearState() { @@ -145,73 +128,36 @@ public async Task Complete_WithStaleId_ShouldNotCompleteCurrentModalsTask() } [Fact] - public void RegisterActiveAlertHost_WithCurrentId_ShouldExposeHost() + public async Task Show_WhenAnotherModalIsActive_ShouldCompleteFirstWithDefault() { // Arrange var service = new ModalService(); - _ = service.Show(); - var modalId = service.ActiveModalId; - var host = new FakeInlineAlertHost(); + var firstTask = service.Show(); // Act - service.RegisterActiveAlertHost(modalId, host); + var secondTask = service.Show(); // Assert - Assert.True(service.TryGetActiveAlertHost(out var resolved)); - Assert.Same(host, resolved); + var firstResult = await firstTask; + Assert.False(firstResult); + Assert.False(secondTask.IsCompleted); + Assert.Equal(typeof(FakeModalB), service.ActiveModalType); } [Fact] - public void RegisterActiveAlertHost_WithStaleId_ShouldBeNoOp() + public void Show_WhenCalledTwice_ShouldAssignDifferentActiveModalIds() { - // Arrange — modal A registers but is replaced by modal B before registration runs. + // Arrange var service = new ModalService(); - _ = service.Show(); - var staleId = service.ActiveModalId; - _ = service.Show(); - - var staleHost = new FakeInlineAlertHost(); // Act - service.RegisterActiveAlertHost(staleId, staleHost); - - // Assert — current modal has no host yet, and the stale host is not visible. - Assert.False(service.TryGetActiveAlertHost(out var resolved)); - Assert.Null(resolved); - } - - [Fact] - public void Show_WhenAnotherModalIsActive_ShouldClearPreviousAlertHost() - { - // Arrange — modal A registered as alert host then replaced by modal B. - var service = new ModalService(); _ = service.Show(); var firstId = service.ActiveModalId; - service.RegisterActiveAlertHost(firstId, new FakeInlineAlertHost()); - - // Act - _ = service.Show(); - - // Assert - Assert.False(service.TryGetActiveAlertHost(out var resolved)); - Assert.Null(resolved); - } - - [Fact] - public async Task Show_WhenAnotherModalIsActive_ShouldCompleteFirstWithDefault() - { - // Arrange - var service = new ModalService(); - var firstTask = service.Show(); - - // Act - var secondTask = service.Show(); + _ = service.Show(); + var secondId = service.ActiveModalId; // Assert - var firstResult = await firstTask; - Assert.False(firstResult); - Assert.False(secondTask.IsCompleted); - Assert.Equal(typeof(FakeModalB), service.ActiveModalType); + Assert.NotEqual(firstId, secondId); } [Fact] @@ -235,22 +181,6 @@ public void Show_WhenCalled_ShouldSetActiveStateAndRaiseStateChanged() Assert.Equal(1, stateChangedCount); } - [Fact] - public void Show_WhenCalledTwice_ShouldAssignDifferentActiveModalIds() - { - // Arrange - var service = new ModalService(); - - // Act - _ = service.Show(); - var firstId = service.ActiveModalId; - _ = service.Show(); - var secondId = service.ActiveModalId; - - // Assert - Assert.NotEqual(firstId, secondId); - } - [Fact] public async Task Show_WhenSameTypeReopened_ShouldCreateFreshTaskWithNewId() { @@ -272,34 +202,6 @@ public async Task Show_WhenSameTypeReopened_ShouldCreateFreshTaskWithNewId() Assert.False(secondTask.IsCompleted); } - [Fact] - public void UnregisterActiveAlertHost_WithStaleId_ShouldNotRemoveCurrentHost() - { - // Arrange — modal A registers, modal B replaces it and registers, then A's late - // unregister fires. B's host must remain. - var service = new ModalService(); - _ = service.Show(); - var firstId = service.ActiveModalId; - - _ = service.Show(); - var secondId = service.ActiveModalId; - var secondHost = new FakeInlineAlertHost(); - service.RegisterActiveAlertHost(secondId, secondHost); - - // Act - service.UnregisterActiveAlertHost(firstId); - - // Assert - Assert.True(service.TryGetActiveAlertHost(out var resolved)); - Assert.Same(secondHost, resolved); - } - - private sealed class FakeInlineAlertHost : IInlineAlertHost - { - public Task ShowInlineAlertAsync(InlineAlertRequest request, CancellationToken cancellationToken) => - Task.FromResult(new InlineAlertResult(true, null)); - } - private sealed class FakeModalA : IComponent { public void Attach(RenderHandle renderHandle) { } diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotesMarkdownRendererTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesMarkdownRendererTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotesMarkdownRendererTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesMarkdownRendererTests.cs index 1f2f05db..e4cf7509 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotesMarkdownRendererTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesMarkdownRendererTests.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.ReleaseNotes; public sealed class ReleaseNotesMarkdownRendererTests { @@ -50,20 +50,20 @@ public void RenderToHtml_BlankLineSeparatesParagraphs() } [Fact] - public void RenderToHtml_Bold_RendersStrong() + public void RenderToHtml_BoldDoesNotInterfereWithItalic() { - var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**bold text**"); + var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**bold** and *italic*"); - Assert.Contains("bold text", html); + Assert.Contains("bold", html); + Assert.Contains("italic", html); } [Fact] - public void RenderToHtml_BoldDoesNotInterfereWithItalic() + public void RenderToHtml_Bold_RendersStrong() { - var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**bold** and *italic*"); + var html = ReleaseNotesMarkdownRenderer.RenderToHtml("**bold text**"); - Assert.Contains("bold", html); - Assert.Contains("italic", html); + Assert.Contains("bold text", html); } [Theory] @@ -316,14 +316,6 @@ public void RenderToHtml_UnderscoreAdjacentToNonAsciiLetter_NotItalicized() Assert.Contains("_au_lait", html); } - [Fact] - public void RenderToHtml_UnderscoreBold_RendersStrong() - { - var html = ReleaseNotesMarkdownRenderer.RenderToHtml("__bold text__"); - - Assert.Contains("bold text", html); - } - [Fact] public void RenderToHtml_UnderscoreBoldMixedWithAsteriskItalic_BothRender() { @@ -334,11 +326,11 @@ public void RenderToHtml_UnderscoreBoldMixedWithAsteriskItalic_BothRender() } [Fact] - public void RenderToHtml_UnderscoreItalic_RendersEm() + public void RenderToHtml_UnderscoreBold_RendersStrong() { - var html = ReleaseNotesMarkdownRenderer.RenderToHtml("paragraph with _italic_ word"); + var html = ReleaseNotesMarkdownRenderer.RenderToHtml("__bold text__"); - Assert.Contains("italic", html); + Assert.Contains("bold text", html); } [Fact] @@ -349,6 +341,14 @@ public void RenderToHtml_UnderscoreItalicAtStartOfLine_RendersEm() Assert.Contains("All changes since the last stable release.", html); } + [Fact] + public void RenderToHtml_UnderscoreItalic_RendersEm() + { + var html = ReleaseNotesMarkdownRenderer.RenderToHtml("paragraph with _italic_ word"); + + Assert.Contains("italic", html); + } + [Fact] public void RenderToHtml_UnmatchedBoldMarkers_LeftAsLiteral() { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotesNormalizerTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotesNormalizerTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs index aba61e55..988f1ea4 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotesNormalizerTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs @@ -4,7 +4,7 @@ using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.ReleaseNotes; public sealed class ReleaseNotesNormalizerTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/SettingsServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/SettingsServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs index 5d5ae28c..c446a1f3 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/SettingsServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs @@ -7,7 +7,7 @@ using Microsoft.Extensions.Logging; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Settings; public sealed class SettingsServiceTests { @@ -70,20 +70,6 @@ public void CopyType_WhenPreferenceIsNull_ShouldReturnDefault() Assert.Equal(CopyType.Default, result); } - [Fact] - public void CopyType_WhenSet_ShouldUpdatePreferences() - { - // Arrange - var mockPreferences = Substitute.For(); - var settingsService = CreateSettingsService(mockPreferences); - - // Act - settingsService.CopyType = CopyType.Full; - - // Assert - mockPreferences.Received(1).KeyboardCopyTypePreference = CopyType.Full; - } - [Fact] public void CopyType_WhenSetToDifferentValue_ShouldInvokeChangedEvent() { @@ -159,6 +145,20 @@ public void CopyType_WhenSetToSameValue_ShouldNotUpdatePreferences() mockPreferences.DidNotReceive().KeyboardCopyTypePreference = Arg.Any(); } + [Fact] + public void CopyType_WhenSet_ShouldUpdatePreferences() + { + // Arrange + var mockPreferences = Substitute.For(); + var settingsService = CreateSettingsService(mockPreferences); + + // Act + settingsService.CopyType = CopyType.Full; + + // Assert + mockPreferences.Received(1).KeyboardCopyTypePreference = CopyType.Full; + } + [Fact] public void IsPreReleaseEnabled_WhenAccessedMultipleTimes_ShouldCacheValue() { @@ -209,35 +209,35 @@ public void IsPreReleaseEnabled_WhenPreferenceIsDefault_ShouldReturnFalse() } [Fact] - public void IsPreReleaseEnabled_WhenSet_ShouldUpdatePreferences() + public void IsPreReleaseEnabled_WhenSetToSameValue_ShouldNotUpdatePreferences() { // Arrange var mockPreferences = Substitute.For(); + mockPreferences.PreReleasePreference.Returns(true); + var settingsService = CreateSettingsService(mockPreferences); + _ = settingsService.IsPreReleaseEnabled; // Cache the value + mockPreferences.ClearReceivedCalls(); // Act settingsService.IsPreReleaseEnabled = true; // Assert - mockPreferences.Received(1).PreReleasePreference = true; + mockPreferences.DidNotReceive().PreReleasePreference = Arg.Any(); } [Fact] - public void IsPreReleaseEnabled_WhenSetToSameValue_ShouldNotUpdatePreferences() + public void IsPreReleaseEnabled_WhenSet_ShouldUpdatePreferences() { // Arrange var mockPreferences = Substitute.For(); - mockPreferences.PreReleasePreference.Returns(true); - var settingsService = CreateSettingsService(mockPreferences); - _ = settingsService.IsPreReleaseEnabled; // Cache the value - mockPreferences.ClearReceivedCalls(); // Act settingsService.IsPreReleaseEnabled = true; // Assert - mockPreferences.DidNotReceive().PreReleasePreference = Arg.Any(); + mockPreferences.Received(1).PreReleasePreference = true; } [Fact] @@ -289,20 +289,6 @@ public void LogLevel_WhenPreferenceIsDefault_ShouldReturnTrace() Assert.Equal(LogLevel.Trace, result); } - [Fact] - public void LogLevel_WhenSet_ShouldUpdatePreferences() - { - // Arrange - var mockPreferences = Substitute.For(); - var settingsService = CreateSettingsService(mockPreferences); - - // Act - settingsService.LogLevel = LogLevel.Warning; - - // Assert - mockPreferences.Received(1).LogLevelPreference = LogLevel.Warning; - } - [Fact] public void LogLevel_WhenSetToDifferentValue_ShouldInvokeChangedEvent() { @@ -378,6 +364,20 @@ public void LogLevel_WhenSetToSameValue_ShouldNotUpdatePreferences() mockPreferences.DidNotReceive().LogLevelPreference = Arg.Any(); } + [Fact] + public void LogLevel_WhenSet_ShouldUpdatePreferences() + { + // Arrange + var mockPreferences = Substitute.For(); + var settingsService = CreateSettingsService(mockPreferences); + + // Act + settingsService.LogLevel = LogLevel.Warning; + + // Assert + mockPreferences.Received(1).LogLevelPreference = LogLevel.Warning; + } + [Fact] public void ShowDisplayPaneOnSelectionChange_WhenAccessedMultipleTimes_ShouldCacheValue() { @@ -428,35 +428,35 @@ public void ShowDisplayPaneOnSelectionChange_WhenPreferenceIsDefault_ShouldRetur } [Fact] - public void ShowDisplayPaneOnSelectionChange_WhenSet_ShouldUpdatePreferences() + public void ShowDisplayPaneOnSelectionChange_WhenSetToSameValue_ShouldNotUpdatePreferences() { // Arrange var mockPreferences = Substitute.For(); + mockPreferences.DisplayPaneSelectionPreference.Returns(true); + var settingsService = CreateSettingsService(mockPreferences); + _ = settingsService.ShowDisplayPaneOnSelectionChange; // Cache the value + mockPreferences.ClearReceivedCalls(); // Act settingsService.ShowDisplayPaneOnSelectionChange = true; // Assert - mockPreferences.Received(1).DisplayPaneSelectionPreference = true; + mockPreferences.DidNotReceive().DisplayPaneSelectionPreference = Arg.Any(); } [Fact] - public void ShowDisplayPaneOnSelectionChange_WhenSetToSameValue_ShouldNotUpdatePreferences() + public void ShowDisplayPaneOnSelectionChange_WhenSet_ShouldUpdatePreferences() { // Arrange var mockPreferences = Substitute.For(); - mockPreferences.DisplayPaneSelectionPreference.Returns(true); - var settingsService = CreateSettingsService(mockPreferences); - _ = settingsService.ShowDisplayPaneOnSelectionChange; // Cache the value - mockPreferences.ClearReceivedCalls(); // Act settingsService.ShowDisplayPaneOnSelectionChange = true; // Assert - mockPreferences.DidNotReceive().DisplayPaneSelectionPreference = Arg.Any(); + mockPreferences.Received(1).DisplayPaneSelectionPreference = true; } [Fact] @@ -508,20 +508,6 @@ public void Theme_WhenPreferenceIsDefault_ShouldReturnSystem() Assert.Equal(Theme.System, result); } - [Fact] - public void Theme_WhenSet_ShouldUpdatePreferences() - { - // Arrange - var mockPreferences = Substitute.For(); - var settingsService = CreateSettingsService(mockPreferences); - - // Act - settingsService.Theme = Theme.Dark; - - // Assert - mockPreferences.Received(1).ThemePreference = Theme.Dark; - } - [Fact] public void Theme_WhenSetToDifferentValue_ShouldInvokeChangedEvent() { @@ -594,6 +580,20 @@ public void Theme_WhenSetToSameValue_ShouldNotUpdatePreferences() mockPreferences.DidNotReceive().ThemePreference = Arg.Any(); } + [Fact] + public void Theme_WhenSet_ShouldUpdatePreferences() + { + // Arrange + var mockPreferences = Substitute.For(); + var settingsService = CreateSettingsService(mockPreferences); + + // Act + settingsService.Theme = Theme.Dark; + + // Assert + mockPreferences.Received(1).ThemePreference = Theme.Dark; + } + [Fact] public void TimeZoneId_WhenAccessedMultipleTimes_ShouldCacheValue() { @@ -644,20 +644,6 @@ public void TimeZoneId_WhenPreferenceIsNull_ShouldReturnLocalTimeZone() Assert.Equal(TimeZoneInfo.Local.Id, result); } - [Fact] - public void TimeZoneId_WhenSet_ShouldUpdatePreferences() - { - // Arrange - var mockPreferences = Substitute.For(); - var settingsService = CreateSettingsService(mockPreferences); - - // Act - settingsService.TimeZoneId = Constants.TimeZoneEastern; - - // Assert - mockPreferences.Received(1).TimeZonePreference = Constants.TimeZoneEastern; - } - [Fact] public void TimeZoneId_WhenSetToDifferentValue_ShouldInvokeChangedEvent() { @@ -721,6 +707,20 @@ public void TimeZoneId_WhenSetToSameValue_ShouldNotUpdatePreferences() mockPreferences.DidNotReceive().TimeZonePreference = Arg.Any(); } + [Fact] + public void TimeZoneId_WhenSet_ShouldUpdatePreferences() + { + // Arrange + var mockPreferences = Substitute.For(); + var settingsService = CreateSettingsService(mockPreferences); + + // Act + settingsService.TimeZoneId = Constants.TimeZoneEastern; + + // Assert + mockPreferences.Received(1).TimeZonePreference = Constants.TimeZoneEastern; + } + [Fact] public void TimeZoneInfo_WhenTimeZoneIdChanges_ShouldReturnUpdatedTimeZone() { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/AppTitleServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/AppTitleServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs index 3f038722..b8dbc016 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/AppTitleServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; @@ -6,7 +6,7 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.System; public sealed class AppTitleServiceTests { @@ -226,7 +226,7 @@ public void SetProgressString_WhenNullProgress_ShouldSetTitle() } [Fact] - public void SetProgressString_WhenProgress_ShouldSetTitle() + public void SetProgressString_WhenProgressAndLogName_ShouldSetTitleWithBoth() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -239,15 +239,17 @@ public void SetProgressString_WhenProgress_ShouldSetTitle() mockTitleProvider); // Act + titleService.SetLogName(Constants.LogName); titleService.SetProgressString(Constants.Percentage); // Assert mockTitleProvider.Received(1) - .SetTitle($"{Constants.Percentage} - {Constants.AppName} {Constants.AppInstalledVersion}"); + .SetTitle( + $"{Constants.Percentage} - {Constants.AppName} {Constants.AppInstalledVersion} - {Constants.LogName}"); } [Fact] - public void SetProgressString_WhenProgressAndLogName_ShouldSetTitleWithBoth() + public void SetProgressString_WhenProgress_ShouldSetTitle() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -260,13 +262,11 @@ public void SetProgressString_WhenProgressAndLogName_ShouldSetTitleWithBoth() mockTitleProvider); // Act - titleService.SetLogName(Constants.LogName); titleService.SetProgressString(Constants.Percentage); // Assert mockTitleProvider.Received(1) - .SetTitle( - $"{Constants.Percentage} - {Constants.AppName} {Constants.AppInstalledVersion} - {Constants.LogName}"); + .SetTitle($"{Constants.Percentage} - {Constants.AppName} {Constants.AppInstalledVersion}"); } private static AppTitleService CreateAppTitleService( diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/GitHubServiceTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Update/GitHubServiceTests.cs index cc5cc109..b40a1a02 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/GitHubServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/GitHubServiceTests.cs @@ -1,4 +1,4 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; @@ -9,7 +9,7 @@ using System.Net; using System.Text.Json; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Update; public sealed class GitHubServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs index 22f524b4..c0088d54 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/UpdateServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs @@ -9,7 +9,7 @@ using EventLogExpert.UI.Tests.TestUtils.Constants; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services; +namespace EventLogExpert.UI.Tests.Services.Update; public sealed class UpdateServiceTests { @@ -61,7 +61,7 @@ public async Task CheckForUpdates_AutoScanCalledTwice_ShouldOnlyFetchReleasesOnc } [Fact] - public async Task CheckForUpdates_DeploymentThrowsException_ShouldShowAlert() + public async Task CheckForUpdates_DeploymentThrowsExceptionAutoScan_ShouldNotShowAlert() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -70,13 +70,14 @@ public async Task CheckForUpdates_DeploymentThrowsException_ShouldShowAlert() var mockAlertDialogService = Substitute.For(); + // User declines the "Update Available" prompt -> UpdateOnNextRestart is called mockAlertDialogService .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(true)); + .Returns(Task.FromResult(false)); var mockDeploymentService = Substitute.For(); - mockDeploymentService.When(x => x.RestartNowAndUpdate(Arg.Any(), Arg.Any())) + mockDeploymentService.When(x => x.UpdateOnNextRestart(Arg.Any(), Arg.Any())) .Do(_ => throw new InvalidOperationException("Deployment failed")); var mockGitHubService = Substitute.For(); @@ -89,15 +90,15 @@ public async Task CheckForUpdates_DeploymentThrowsException_ShouldShowAlert() alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: true); + await updateService.CheckForUpdates(false, userInitiated: false); // Assert - await mockAlertDialogService.Received(1) - .ShowAlert("Update Failure", Arg.Is(s => s.Contains("Deployment failed")), "OK"); + await mockAlertDialogService.DidNotReceive() + .ShowAlert("Update Failure", Arg.Any(), "OK"); } [Fact] - public async Task CheckForUpdates_DeploymentThrowsExceptionAutoScan_ShouldNotShowAlert() + public async Task CheckForUpdates_DeploymentThrowsException_ShouldShowAlert() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -106,14 +107,13 @@ public async Task CheckForUpdates_DeploymentThrowsExceptionAutoScan_ShouldNotSho var mockAlertDialogService = Substitute.For(); - // User declines the "Update Available" prompt -> UpdateOnNextRestart is called mockAlertDialogService .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(false)); + .Returns(Task.FromResult(true)); var mockDeploymentService = Substitute.For(); - mockDeploymentService.When(x => x.UpdateOnNextRestart(Arg.Any(), Arg.Any())) + mockDeploymentService.When(x => x.RestartNowAndUpdate(Arg.Any(), Arg.Any())) .Do(_ => throw new InvalidOperationException("Deployment failed")); var mockGitHubService = Substitute.For(); @@ -126,11 +126,11 @@ public async Task CheckForUpdates_DeploymentThrowsExceptionAutoScan_ShouldNotSho alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: false); + await updateService.CheckForUpdates(false, userInitiated: true); // Assert - await mockAlertDialogService.DidNotReceive() - .ShowAlert("Update Failure", Arg.Any(), "OK"); + await mockAlertDialogService.Received(1) + .ShowAlert("Update Failure", Arg.Is(s => s.Contains("Deployment failed")), "OK"); } [Fact] @@ -187,7 +187,7 @@ await mockAlertDialogService.Received(1).ShowAlert( } [Fact] - public async Task CheckForUpdates_GetReleasesThrowsException_ShouldShowAlert() + public async Task CheckForUpdates_GetReleasesThrowsExceptionAutoScan_ShouldNotShowAlert() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -209,18 +209,18 @@ public async Task CheckForUpdates_GetReleasesThrowsException_ShouldShowAlert() alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: true); + await updateService.CheckForUpdates(false, userInitiated: false); // Assert - await mockAlertDialogService.Received(1) - .ShowAlert("Update Failure", Arg.Is(s => s.Contains("Network error")), "OK"); + await mockAlertDialogService.DidNotReceive() + .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any()); mockDeploymentService.DidNotReceive().RestartNowAndUpdate(Arg.Any(), Arg.Any()); mockDeploymentService.DidNotReceive().UpdateOnNextRestart(Arg.Any(), Arg.Any()); } [Fact] - public async Task CheckForUpdates_GetReleasesThrowsExceptionAutoScan_ShouldNotShowAlert() + public async Task CheckForUpdates_GetReleasesThrowsException_ShouldShowAlert() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -242,11 +242,11 @@ public async Task CheckForUpdates_GetReleasesThrowsExceptionAutoScan_ShouldNotSh alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: false); + await updateService.CheckForUpdates(false, userInitiated: true); // Assert - await mockAlertDialogService.DidNotReceive() - .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any()); + await mockAlertDialogService.Received(1) + .ShowAlert("Update Failure", Arg.Is(s => s.Contains("Network error")), "OK"); mockDeploymentService.DidNotReceive().RestartNowAndUpdate(Arg.Any(), Arg.Any()); mockDeploymentService.DidNotReceive().UpdateOnNextRestart(Arg.Any(), Arg.Any()); @@ -358,7 +358,7 @@ public async Task CheckForUpdates_ManualThenAutoScan_ShouldRunBoth() } [Fact] - public async Task CheckForUpdates_NoReleases_ShouldShowAlert() + public async Task CheckForUpdates_NoReleasesAutoScan_ShouldNotShowAlert() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -378,18 +378,18 @@ public async Task CheckForUpdates_NoReleases_ShouldShowAlert() alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: true); + await updateService.CheckForUpdates(false, userInitiated: false); // Assert mockDeploymentService.DidNotReceive().RestartNowAndUpdate(Arg.Any(), Arg.Any()); mockDeploymentService.DidNotReceive().UpdateOnNextRestart(Arg.Any(), Arg.Any()); - await mockAlertDialogService.Received(1) - .ShowAlert("Update Failure", Arg.Is(s => s.Contains("No releases available")), "OK"); + await mockAlertDialogService.DidNotReceive() + .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any()); } [Fact] - public async Task CheckForUpdates_NoReleasesAutoScan_ShouldNotShowAlert() + public async Task CheckForUpdates_NoReleases_ShouldShowAlert() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -409,14 +409,14 @@ public async Task CheckForUpdates_NoReleasesAutoScan_ShouldNotShowAlert() alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: false); + await updateService.CheckForUpdates(false, userInitiated: true); // Assert mockDeploymentService.DidNotReceive().RestartNowAndUpdate(Arg.Any(), Arg.Any()); mockDeploymentService.DidNotReceive().UpdateOnNextRestart(Arg.Any(), Arg.Any()); - await mockAlertDialogService.DidNotReceive() - .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any()); + await mockAlertDialogService.Received(1) + .ShowAlert("Update Failure", Arg.Is(s => s.Contains("No releases available")), "OK"); } [Fact] @@ -479,19 +479,13 @@ public async Task CheckForUpdates_OnCurrentPrerelease_ShouldSetPrereleaseFlag() } [Fact] - public async Task CheckForUpdates_Prerelease_ShouldUpdateImmediately() + public async Task CheckForUpdates_PreRelease_ShouldUpdateOnNextRestart() { // Arrange var mockCurrentVersionProvider = Substitute.For(); mockCurrentVersionProvider.CurrentVersion.Returns(new Version(Constants.AppInstalledVersion)); mockCurrentVersionProvider.IsDevBuild.Returns(false); - var mockAlertDialogService = Substitute.For(); - - mockAlertDialogService - .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) - .Returns(Task.FromResult(true)); - var mockGitHubService = Substitute.For(); mockGitHubService.GetReleases().Returns(Task.FromResult(GitHubUtils.CreateGitReleaseModels())); @@ -500,24 +494,29 @@ public async Task CheckForUpdates_Prerelease_ShouldUpdateImmediately() var updateService = CreateUpdateService( mockCurrentVersionProvider, gitHubService: mockGitHubService, - deploymentService: mockDeploymentService, - alertDialogService: mockAlertDialogService); + deploymentService: mockDeploymentService); // Act await updateService.CheckForUpdates(true, false); // Assert - mockDeploymentService.Received(1).RestartNowAndUpdate(Constants.GitHubPrereleaseUri, userInitiated: true); + mockDeploymentService.Received(1).UpdateOnNextRestart(Constants.GitHubPrereleaseUri, userInitiated: false); } [Fact] - public async Task CheckForUpdates_PreRelease_ShouldUpdateOnNextRestart() + public async Task CheckForUpdates_Prerelease_ShouldUpdateImmediately() { // Arrange var mockCurrentVersionProvider = Substitute.For(); mockCurrentVersionProvider.CurrentVersion.Returns(new Version(Constants.AppInstalledVersion)); mockCurrentVersionProvider.IsDevBuild.Returns(false); + var mockAlertDialogService = Substitute.For(); + + mockAlertDialogService + .ShowAlert(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.FromResult(true)); + var mockGitHubService = Substitute.For(); mockGitHubService.GetReleases().Returns(Task.FromResult(GitHubUtils.CreateGitReleaseModels())); @@ -526,17 +525,18 @@ public async Task CheckForUpdates_PreRelease_ShouldUpdateOnNextRestart() var updateService = CreateUpdateService( mockCurrentVersionProvider, gitHubService: mockGitHubService, - deploymentService: mockDeploymentService); + deploymentService: mockDeploymentService, + alertDialogService: mockAlertDialogService); // Act await updateService.CheckForUpdates(true, false); // Assert - mockDeploymentService.Received(1).UpdateOnNextRestart(Constants.GitHubPrereleaseUri, userInitiated: false); + mockDeploymentService.Received(1).RestartNowAndUpdate(Constants.GitHubPrereleaseUri, userInitiated: true); } [Fact] - public async Task CheckForUpdates_UserDeclinesUpdate_ShouldUpdateOnNextRestart() + public async Task CheckForUpdates_UserDeclinesUpdateManualScan_ShouldPropagateUserInitiated() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -561,15 +561,14 @@ public async Task CheckForUpdates_UserDeclinesUpdate_ShouldUpdateOnNextRestart() alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, false); + await updateService.CheckForUpdates(false, userInitiated: true); // Assert - mockDeploymentService.DidNotReceive().RestartNowAndUpdate(Arg.Any(), Arg.Any()); - mockDeploymentService.Received(1).UpdateOnNextRestart(Constants.GitHubLatestUri, userInitiated: false); + mockDeploymentService.Received(1).UpdateOnNextRestart(Constants.GitHubLatestUri, userInitiated: true); } [Fact] - public async Task CheckForUpdates_UserDeclinesUpdateManualScan_ShouldPropagateUserInitiated() + public async Task CheckForUpdates_UserDeclinesUpdate_ShouldUpdateOnNextRestart() { // Arrange var mockCurrentVersionProvider = Substitute.For(); @@ -594,10 +593,11 @@ public async Task CheckForUpdates_UserDeclinesUpdateManualScan_ShouldPropagateUs alertDialogService: mockAlertDialogService); // Act - await updateService.CheckForUpdates(false, userInitiated: true); + await updateService.CheckForUpdates(false, false); // Assert - mockDeploymentService.Received(1).UpdateOnNextRestart(Constants.GitHubLatestUri, userInitiated: true); + mockDeploymentService.DidNotReceive().RestartNowAndUpdate(Arg.Any(), Arg.Any()); + mockDeploymentService.Received(1).UpdateOnNextRestart(Constants.GitHubLatestUri, userInitiated: false); } [Fact] diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs similarity index 81% rename from tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs index 0eacd759..e8c90f93 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs @@ -18,10 +18,13 @@ using Microsoft.Extensions.DependencyInjection; using NSubstitute; using System.Collections.Immutable; +using CloseAllAction = EventLogExpert.UI.Store.EventTable.CloseAllAction; +using CloseLogAction = EventLogExpert.UI.Store.EventLog.CloseLogAction; +using Effects = EventLogExpert.UI.Store.EventLog.Effects; namespace EventLogExpert.UI.Tests.Store.EventLog; -public sealed class EventLogEffectsTests +public sealed class EffectsTests { [Fact] public async Task HandleAddEvent_WhenBufferReachesMaxEvents_ShouldSetFullFlag() @@ -37,13 +40,13 @@ public async Task HandleAddEvent_WhenBufferReachesMaxEvents_ShouldSetFullFlag() var (effects, mockDispatcher) = CreateEffects(false, activeLogs, existingBuffer); var newEvent = EventUtils.CreateTestEvent(1000, logName: Constants.LogNameTestLog); - var action = new EventLogAction.AddEvent(newEvent); + var action = new AddEventAction(newEvent); // Act await effects.HandleAddEvent(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.IsFull == true && a.UpdatedBuffer.Count == EventLogState.MaxNewEvents)); } @@ -59,13 +62,13 @@ public async Task HandleAddEvent_WhenContinuouslyUpdateFalse_ShouldBufferEvent() activeLogs); var newEvent = EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog); - var action = new EventLogAction.AddEvent(newEvent); + var action = new AddEventAction(newEvent); // Act await effects.HandleAddEvent(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.UpdatedBuffer.Count == 1 && a.UpdatedBuffer[0] == newEvent)); } @@ -84,15 +87,15 @@ public async Task HandleAddEvent_WhenContinuouslyUpdateTrue_AndEventFilteredOut_ .Returns(new List()); var newEvent = EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog); - var action = new EventLogAction.AddEvent(newEvent); + var action = new AddEventAction(newEvent); // Act await effects.HandleAddEvent(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -107,16 +110,16 @@ public async Task HandleAddEvent_WhenContinuouslyUpdateTrue_ShouldDispatchSucces activeLogs); var newEvent = EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog); - var action = new EventLogAction.AddEvent(newEvent); + var action = new AddEventAction(newEvent); // Act await effects.HandleAddEvent(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogId == logData.Id && a.Events.Count == 1 && a.Events[0] == newEvent)); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -125,7 +128,7 @@ public async Task HandleAddEvent_WhenLogNotActive_ShouldNotDispatchActions() // Arrange var (effects, mockDispatcher) = CreateEffects(); var newEvent = EventUtils.CreateTestEvent(100, logName: Constants.LogNameTestLog); - var action = new EventLogAction.AddEvent(newEvent); + var action = new AddEventAction(newEvent); // Act await effects.HandleAddEvent(action, mockDispatcher); @@ -153,7 +156,7 @@ public async Task HandleCloseAll_ShouldClearAllResolvedXml() mockServiceScopeFactory.CreateScope().Returns(mockServiceScope); mockServiceScope.ServiceProvider.Returns(Substitute.For()); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, Substitute.For(), Substitute.For(), @@ -186,8 +189,8 @@ public async Task HandleCloseAll_ShouldRemoveAllLogsAndClearCache() // Assert await mockLogWatcher.Received(1).RemoveAllAsync(); mockResolverCache.Received(1).ClearAll(); - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -203,7 +206,7 @@ public async Task HandleCloseLog_AwaitsWatcherShutdown_BeforeSignalingCloseCompl mockLogWatcher.RemoveLogAsync(Arg.Any()).Returns(watcherTcs.Task); var logId = EventLogId.Create(); - var action = new EventLogAction.CloseLog(logId, Constants.LogNameTestLog); + var action = new CloseLogAction(logId, Constants.LogNameTestLog); // Act var closeTask = effects.HandleCloseLog(action, mockDispatcher); @@ -243,7 +246,7 @@ public async Task HandleCloseLog_ShouldClearResolvedXmlForLog() mockServiceScopeFactory.CreateScope().Returns(mockServiceScope); mockServiceScope.ServiceProvider.Returns(Substitute.For()); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, Substitute.For(), Substitute.For(), @@ -256,7 +259,7 @@ public async Task HandleCloseLog_ShouldClearResolvedXmlForLog() Substitute.For()); var mockDispatcher = Substitute.For(); - var action = new EventLogAction.CloseLog(logId, Constants.LogNameTestLog); + var action = new CloseLogAction(logId, Constants.LogNameTestLog); // Act await effects.HandleCloseLog(action, mockDispatcher); @@ -271,7 +274,7 @@ public async Task HandleCloseLog_ShouldRemoveLogAndDispatchCloseAction() // Arrange var logId = EventLogId.Create(); var (effects, mockDispatcher, mockLogWatcher, _, _) = CreateEffectsWithServices(); - var action = new EventLogAction.CloseLog(logId, Constants.LogNameTestLog); + var action = new CloseLogAction(logId, Constants.LogNameTestLog); // Act await effects.HandleCloseLog(action, mockDispatcher); @@ -279,7 +282,7 @@ public async Task HandleCloseLog_ShouldRemoveLogAndDispatchCloseAction() // Assert await mockLogWatcher.Received(1).RemoveLogAsync(Constants.LogNameTestLog); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogId == logId)); } @@ -289,7 +292,7 @@ public async Task HandleCloseLog_WhenLastLog_ShouldClearResolverCache() // Arrange — state has no active logs (reducer already removed the last one) var logId = EventLogId.Create(); var (effects, mockDispatcher, mockLogWatcher, mockResolverCache, _) = CreateEffectsWithServices(); - var action = new EventLogAction.CloseLog(logId, Constants.LogNameTestLog); + var action = new CloseLogAction(logId, Constants.LogNameTestLog); // Act await effects.HandleCloseLog(action, mockDispatcher); @@ -308,7 +311,7 @@ public async Task HandleCloseLog_WhenOtherLogsRemain_ShouldNotClearResolverCache var (effects, mockDispatcher, _, mockResolverCache, _) = CreateEffectsWithServices(activeLogs: activeLogs); var closingLogId = EventLogId.Create(); - var action = new EventLogAction.CloseLog(closingLogId, Constants.LogNameTestLog); + var action = new CloseLogAction(closingLogId, Constants.LogNameTestLog); // Act await effects.HandleCloseLog(action, mockDispatcher); @@ -329,14 +332,14 @@ public async Task HandleLoadEvents_ShouldFilterAndDispatchUpdateTable() var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var (effects, mockDispatcher, _, _, mockFilterService) = CreateEffectsWithServices(); - var action = new EventLogAction.LoadEvents(logData, events); + var action = new LoadEventsAction(logData, events); // Act await effects.HandleLoadEvents(action, mockDispatcher); // Assert mockFilterService.Received(1).GetFilteredEvents(events, Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -360,14 +363,14 @@ public async Task HandleLoadNewEvents_ShouldProcessBufferAndDispatchActions() await effects.HandleLoadNewEvents(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.EventsByLog.Count == 1 && a.EventsByLog.ContainsKey(logData.Id) && a.EventsByLog[logData.Id].Count == 2)); - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.UpdatedBuffer.Count == 0 && a.IsFull == false)); } @@ -393,9 +396,9 @@ public async Task HandleLoadNewEvents_WhenAllEventsFiltered_ShouldNotDispatchApp await effects.HandleLoadNewEvents(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.UpdatedBuffer.Count == 0 && a.IsFull == false)); } @@ -423,17 +426,17 @@ public async Task HandleLoadNewEvents_WhenBufferSpansMultipleLogs_ShouldGroupInt newEventBuffer: bufferedEvents); // Capture the dispatched batch for inspection. - EventTableAction.AppendTableEventsBatch? captured = null; + AppendTableEventsBatchAction? captured = null; mockDispatcher - .When(dispatcher => dispatcher.Dispatch(Arg.Any())) - .Do(call => captured = (EventTableAction.AppendTableEventsBatch)call.Args()[0]); + .When(dispatcher => dispatcher.Dispatch(Arg.Any())) + .Do(call => captured = (AppendTableEventsBatchAction)call.Args()[0]); // Act await effects.HandleLoadNewEvents(mockDispatcher); // Assert: a single batched append covering both logs. - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); Assert.NotNull(captured); Assert.Equal(2, captured!.EventsByLog.Count); Assert.Equal(2, captured.EventsByLog[applicationLog.Id].Count); @@ -457,7 +460,7 @@ public async Task HandleOpenLog_AwaitsInitialClassificationTask_BeforeResolverCo mockDatabaseService.InitialClassificationTask.Returns(classificationTcs.Task); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); + var action = new OpenLogAction(Constants.LogNameApplication, LogPathType.Channel); // Act 1 — start the open; await yields back at InitialClassificationTask. var openTask = effects.HandleOpenLog(action, mockDispatcher); @@ -508,7 +511,7 @@ public async Task HandleOpenLog_LogClosedDuringClassificationAwait_DoesNotDispat var mockDatabaseService = Substitute.For(); mockDatabaseService.InitialClassificationTask.Returns(classificationTcs.Task); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, Substitute.For(), Substitute.For(), @@ -521,7 +524,7 @@ public async Task HandleOpenLog_LogClosedDuringClassificationAwait_DoesNotDispat Substitute.For()); var mockDispatcher = Substitute.For(); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); + var action = new OpenLogAction(Constants.LogNameApplication, LogPathType.Channel); // Act 1 — start the open; await yields back at InitialClassificationTask. var openTask = effects.HandleOpenLog(action, mockDispatcher); @@ -541,7 +544,7 @@ public async Task HandleOpenLog_LogClosedDuringClassificationAwait_DoesNotDispat // happened. The bail-out path returns silently after the post-await identity check. mockServiceProvider.DidNotReceive().GetService(typeof(IEventResolver)); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -559,7 +562,7 @@ public async Task HandleOpenLog_ResolverThrows_CallsReportCritical_DoesNotPropag var thrown = new InvalidOperationException("resolver factory failed"); mockServiceProvider.When(p => p.GetService(typeof(IEventResolver))).Do(_ => throw thrown); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); + var action = new OpenLogAction(Constants.LogNameApplication, LogPathType.Channel); // Act — must not throw. await effects.HandleOpenLog(action, mockDispatcher); @@ -567,7 +570,7 @@ public async Task HandleOpenLog_ResolverThrows_CallsReportCritical_DoesNotPropag // Assert — exact exception forwarded to banner; no resolver-status dispatch fired. mockBannerService.Received(1).ReportCritical(thrown); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -584,14 +587,14 @@ public async Task HandleOpenLog_WhenCancelled_ShouldDispatchCloseAndClearStatus( activeLogs: activeLogs, hasEventResolver: true); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel, cts.Token); + var action = new OpenLogAction(Constants.LogNameApplication, LogPathType.Channel, cts.Token); // Act await effects.HandleOpenLog(action, mockDispatcher); // Assert - mockDispatcher.Received().Dispatch(Arg.Any()); - mockDispatcher.Received().Dispatch(Arg.Any()); + mockDispatcher.Received().Dispatch(Arg.Any()); + mockDispatcher.Received().Dispatch(Arg.Any()); } [Fact] @@ -599,13 +602,13 @@ public async Task HandleOpenLog_WhenLogNotInActiveLogs_ShouldDispatchError() { // Arrange var (effects, mockDispatcher) = CreateEffects(hasEventResolver: true); - var action = new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel); + var action = new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel); // Act await effects.HandleOpenLog(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.ResolverStatus.Contains("Error") && a.ResolverStatus.Contains(Constants.LogNameTestLog))); } @@ -620,13 +623,13 @@ public async Task HandleOpenLog_WhenNoEventResolver_ShouldDispatchError() activeLogs: activeLogs, hasEventResolver: false); - var action = new EventLogAction.OpenLog(Constants.LogNameApplication, LogPathType.Channel); + var action = new OpenLogAction(Constants.LogNameApplication, LogPathType.Channel); // Act await effects.HandleOpenLog(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.ResolverStatus.Contains("Error"))); } @@ -640,13 +643,13 @@ public async Task HandleSetContinuouslyUpdate_WhenFalse_ShouldNotProcessBuffer() }; var (effects, mockDispatcher) = CreateEffects(newEventBuffer: bufferedEvents); - var action = new EventLogAction.SetContinuouslyUpdate(false); + var action = new SetContinuouslyUpdateAction(false); // Act await effects.HandleSetContinuouslyUpdate(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -665,15 +668,15 @@ public async Task HandleSetContinuouslyUpdate_WhenTrue_ShouldProcessBuffer() activeLogs: activeLogs, newEventBuffer: bufferedEvents); - var action = new EventLogAction.SetContinuouslyUpdate(true); + var action = new SetContinuouslyUpdateAction(true); // Act await effects.HandleSetContinuouslyUpdate(action, mockDispatcher); // Assert: ProcessNewEventBuffer now dispatches a batched append (no UpdateDisplayedEvents). - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -685,15 +688,15 @@ public async Task HandleSetFilters_FilterBranch_ShouldDispatchSetIsLoadingTrueTh var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); var filter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); - var action = new EventLogAction.SetFilters(new EventFilter(null, [filter])); + var action = new SetFiltersAction(new EventFilter(null, [filter])); await effects.HandleSetFilters(action, mockDispatcher); Received.InOrder(() => { - mockDispatcher.Dispatch(Arg.Is(a => a.IsLoading)); - mockDispatcher.Dispatch(Arg.Any()); - mockDispatcher.Dispatch(Arg.Is(a => !a.IsLoading)); + mockDispatcher.Dispatch(Arg.Is(a => a.IsLoading)); + mockDispatcher.Dispatch(Arg.Any()); + mockDispatcher.Dispatch(Arg.Is(a => !a.IsLoading)); }); } @@ -711,14 +714,14 @@ public async Task HandleSetFilters_FilterBranch_WhenFilterServiceThrows_ShouldSt .Do(_ => throw new InvalidOperationException("boom")); var filter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); - var action = new EventLogAction.SetFilters(new EventFilter(null, [filter])); + var action = new SetFiltersAction(new EventFilter(null, [filter])); await Assert.ThrowsAsync( () => effects.HandleSetFilters(action, mockDispatcher)); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.IsLoading)); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => !a.IsLoading)); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.IsLoading)); + mockDispatcher.Received(1).Dispatch(Arg.Is(a => !a.IsLoading)); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -760,13 +763,13 @@ public async Task HandleSetFilters_FilterBranch_WhenLogClosedDuringFilter_Should }); var nonXmlFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); - var action = new EventLogAction.SetFilters(new EventFilter(null, [nonXmlFilter])); + var action = new SetFiltersAction(new EventFilter(null, [nonXmlFilter])); // Act await effects.HandleSetFilters(action, mockDispatcher); // Assert — dispatched UpdateDisplayedEvents has no entry for the closed log id. - mockDispatcher.Received(1).Dispatch(Arg.Is( + mockDispatcher.Received(1).Dispatch(Arg.Is( a => !a.ActiveLogs.ContainsKey(snapshotData.Id))); } @@ -824,7 +827,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogEventsChangeDuringFilter_ _ => pass2Result); var nonXmlFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); - var action = new EventLogAction.SetFilters(new EventFilter(null, [nonXmlFilter])); + var action = new SetFiltersAction(new EventFilter(null, [nonXmlFilter])); // Act await effects.HandleSetFilters(action, mockDispatcher); @@ -840,7 +843,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogEventsChangeDuringFilter_ Arg.Is>(logs => logs.Any(l => ReferenceEquals(l.Events, liveTailEvents))), Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Is( + mockDispatcher.Received(1).Dispatch(Arg.Is( a => a.ActiveLogs.ContainsKey(snapshotData.Id) && ReferenceEquals(a.ActiveLogs[snapshotData.Id], liveTailEvents))); } @@ -911,7 +914,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou }); var nonXmlFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); - var action = new EventLogAction.SetFilters(new EventFilter(null, [nonXmlFilter])); + var action = new SetFiltersAction(new EventFilter(null, [nonXmlFilter])); // Act await effects.HandleSetFilters(action, mockDispatcher); @@ -921,7 +924,7 @@ public async Task HandleSetFilters_FilterBranch_WhenLogStillStaleAfterRetry_Shou Arg.Any>(), Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Is( + mockDispatcher.Received(1).Dispatch(Arg.Is( a => !a.ActiveLogs.ContainsKey(snapshotData.Id))); } @@ -970,27 +973,27 @@ public async Task HandleSetFilters_FilterBranch_WhenSupersededByNewerFilter_Shou }); // Start the stale filter and wait until it is parked inside FilterActiveLogs. - var staleTask = effects.HandleSetFilters(new EventLogAction.SetFilters(staleFilter), mockDispatcher); + var staleTask = effects.HandleSetFilters(new SetFiltersAction(staleFilter), mockDispatcher); await staleStarted.Task; // While the stale filter is still parked, run the fresh filter to completion. This // bumps _filterGeneration so the stale run will be detected as superseded. - await effects.HandleSetFilters(new EventLogAction.SetFilters(freshFilter), mockDispatcher); + await effects.HandleSetFilters(new SetFiltersAction(freshFilter), mockDispatcher); // Release the stale filter — its post-await checks should now fail the generation guard. staleGate.SetResult(true); await staleTask; // Fresh result reached the table; stale result was dropped. - mockDispatcher.Received(1).Dispatch(Arg.Is( + mockDispatcher.Received(1).Dispatch(Arg.Is( a => a.ActiveLogs[logData.Id][0].Id == 100)); - mockDispatcher.DidNotReceive().Dispatch(Arg.Is( + mockDispatcher.DidNotReceive().Dispatch(Arg.Is( a => a.ActiveLogs.ContainsKey(logData.Id) && a.ActiveLogs[logData.Id].Any(e => e.Id == 999))); // SetIsLoading(false) was dispatched by the fresh run; the stale run's finally was // suppressed by the generation guard, so we should see exactly one false-dispatch. - mockDispatcher.Received(1).Dispatch(Arg.Is(a => !a.IsLoading)); + mockDispatcher.Received(1).Dispatch(Arg.Is(a => !a.IsLoading)); } [Fact] @@ -1002,12 +1005,12 @@ public async Task HandleSetFilters_ReloadBranch_ShouldNotDispatchSetIsLoading() var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); var xmlFilter = FilterUtils.CreateTestFilter(Constants.FilterXmlContainsData, isEnabled: true); - var action = new EventLogAction.SetFilters(new EventFilter(null, [xmlFilter])); + var action = new SetFiltersAction(new EventFilter(null, [xmlFilter])); await effects.HandleSetFilters(action, mockDispatcher); Assert.True(action.EventFilter.RequiresXml); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -1026,7 +1029,7 @@ public async Task HandleSetFilters_ShouldFilterAndDispatchUpdate() var (effects, mockDispatcher, _, _, mockFilterService) = CreateEffectsWithServices(activeLogs: activeLogs); var eventFilter = new EventFilter(null, []); - var action = new EventLogAction.SetFilters(eventFilter); + var action = new SetFiltersAction(eventFilter); // Act await effects.HandleSetFilters(action, mockDispatcher); @@ -1036,7 +1039,7 @@ public async Task HandleSetFilters_ShouldFilterAndDispatchUpdate() Arg.Any>(), eventFilter); - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -1051,16 +1054,16 @@ public async Task HandleSetFilters_WhenFilterDoesNotRequireXml_ShouldNotReloadLo var nonXmlFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); var eventFilter = new EventFilter(null, [nonXmlFilter]); - var action = new EventLogAction.SetFilters(eventFilter); + var action = new SetFiltersAction(eventFilter); // Act await effects.HandleSetFilters(action, mockDispatcher); // Assert — UpdateDisplayedEvents path; no Close/Open dispatches. Assert.False(eventFilter.RequiresXml); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -1078,15 +1081,15 @@ public async Task HandleSetFilters_WhenFilterRequiresXmlAndLogLacksXml_ShouldClo // being swallowed by the discard. var closeTasks = new List(); mockDispatcher - .When(d => d.Dispatch(Arg.Any())) + .When(d => d.Dispatch(Arg.Any())) .Do(callInfo => { - closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); + closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); }); var xmlFilter = FilterUtils.CreateTestFilter(Constants.FilterXmlContainsData, isEnabled: true); var eventFilter = new EventFilter(null, [xmlFilter]); - var action = new EventLogAction.SetFilters(eventFilter); + var action = new SetFiltersAction(eventFilter); // Act await effects.HandleSetFilters(action, mockDispatcher); @@ -1094,14 +1097,14 @@ public async Task HandleSetFilters_WhenFilterRequiresXmlAndLogLacksXml_ShouldClo // Assert Assert.True(eventFilter.RequiresXml); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogName == Constants.LogNameTestLog && a.LogId == logData.Id)); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogName == Constants.LogNameTestLog && a.LogPathType == LogPathType.Channel)); // Reload path returns early — no UpdateDisplayedEvents until LoadEvents fires. - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); // Surface any HandleCloseLog faults before exiting the test. await Task.WhenAll(closeTasks); @@ -1129,17 +1132,17 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_AwaitsCloseCompletionBe // be silently observed by the finalizer). var closeTasks = new List(); mockDispatcher - .When(d => d.Dispatch(Arg.Any())) + .When(d => d.Dispatch(Arg.Any())) .Do(callInfo => { - closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); + closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); }); var xmlFilter = FilterUtils.CreateTestFilter(Constants.FilterXmlContainsData, isEnabled: true); var eventFilter = new EventFilter(null, [xmlFilter]); // Act — start HandleSetFilters; it must remain pending until watcherCompletion fires. - var setFiltersTask = effects.HandleSetFilters(new EventLogAction.SetFilters(eventFilter), mockDispatcher); + var setFiltersTask = effects.HandleSetFilters(new SetFiltersAction(eventFilter), mockDispatcher); // Assert — task is blocked because HandleCloseLog can't finish until RemoveLogAsync // returns. Without the close-completion await in HandleSetFilters, the task would @@ -1152,7 +1155,7 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_AwaitsCloseCompletionBe await setFiltersTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); // OpenLog should now have been dispatched (only happens after the close await). - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogName == Constants.LogNameTestLog && a.LogPathType == LogPathType.Channel)); // Surface any HandleCloseLog faults before exiting the test. @@ -1191,7 +1194,7 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_ShouldRestoreSelectionA mockServiceScopeFactory.CreateScope().Returns(mockServiceScope); mockServiceScope.ServiceProvider.Returns(mockServiceProvider); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, mockFilterService, Substitute.For(), @@ -1213,17 +1216,17 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_ShouldRestoreSelectionA // the test instead of being swallowed by the discard. var closeTasks = new List(); mockDispatcher - .When(d => d.Dispatch(Arg.Any())) + .When(d => d.Dispatch(Arg.Any())) .Do(callInfo => { - closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); + closeTasks.Add(effects.HandleCloseLog(callInfo.Arg(), mockDispatcher)); }); var xmlFilter = FilterUtils.CreateTestFilter(Constants.FilterXmlContainsData, isEnabled: true); var eventFilter = new EventFilter(null, [xmlFilter]); // Act 1: Apply the XML filter — populates _pendingSelectionRestore for "TestLog". - await effects.HandleSetFilters(new EventLogAction.SetFilters(eventFilter), mockDispatcher); + await effects.HandleSetFilters(new SetFiltersAction(eventFilter), mockDispatcher); // Act 2: Simulate the subsequent LoadEvents that the new OpenLog produces. The // reloaded events include the previously-selected RecordId=42 plus an unrelated one. @@ -1231,99 +1234,16 @@ public async Task HandleSetFilters_WhenFilterRequiresXml_ShouldRestoreSelectionA EventUtils.CreateTestEvent(100, recordId: 42, logName: Constants.LogNameTestLog), EventUtils.CreateTestEvent(200, recordId: 99, logName: Constants.LogNameTestLog)); - await effects.HandleLoadEvents(new EventLogAction.LoadEvents(logData, reloadedEvents), mockDispatcher); + await effects.HandleLoadEvents(new LoadEventsAction(logData, reloadedEvents), mockDispatcher); // Assert — SetSelectedEvents dispatched with exactly the restored event (RecordId=42). - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.SelectedEvents.Count() == 1 && a.SelectedEvents.First().RecordId == 42)); // Surface any HandleCloseLog faults before exiting the test. await Task.WhenAll(closeTasks); } - [Fact] - public async Task PrepareForDatabaseRemovalAsync_DispatchesCloseLogPerActiveLog_PopulatesSnapshotInCloseOrder() - { - // Arrange — two active logs; route the dispatcher's CloseLog actions back into - // HandleCloseLog so the close-completion TCSes get signaled. This proves the sink - // pattern (snapshot is populated incrementally) rather than relying on the live - // Fluxor pipeline. - var log1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var log2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var log1Id = log1.Id; - var log2Id = log2.Id; - - var activeLogs = ImmutableDictionary.Empty - .Add(Constants.LogNameLog1, log1) - .Add(Constants.LogNameLog2, log2); - - var (effects, mockDispatcher, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); - - // Route CloseLog dispatches back into HandleCloseLog so the close-completion TCSes - // get signaled. Without this the coordinator would block forever waiting on the TCSes. - mockDispatcher - .When(d => d.Dispatch(Arg.Any())) - .Do(callInfo => - { - var action = callInfo.Arg(); - _ = effects.HandleCloseLog(action, mockDispatcher); - }); - - var snapshot = new LogReopenSnapshot(); - var coordinator = (ILogReloadCoordinator)effects; - - // Act - await coordinator.PrepareForDatabaseRemovalAsync(snapshot, TestContext.Current.CancellationToken); - - // Assert — snapshot has both logs (order matches the ActiveLogs iteration). - Assert.Equal(2, snapshot.Items.Count); - Assert.Contains(snapshot.Items, item => item.Name == Constants.LogNameLog1 && item.Type == LogPathType.Channel); - Assert.Contains(snapshot.Items, item => item.Name == Constants.LogNameLog2 && item.Type == LogPathType.Channel); - - // Both CloseLog actions should have been dispatched. - mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogId == log1Id)); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogId == log2Id)); - } - - [Fact] - public async Task PrepareForDatabaseRemovalAsync_WhenCancellationTokenCancelsBeforeClose_ThrowsAndDoesNotPopulateSnapshot() - { - // Arrange — one active log, dispatcher does NOT route back. The close-completion - // TCS will never be signaled, so the await with a cancelled token should throw and - // the sink should remain empty. - var log = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameLog1, log); - - var (effects, _, _, _, _) = CreateEffectsWithServices(activeLogs: activeLogs); - var snapshot = new LogReopenSnapshot(); - var coordinator = (ILogReloadCoordinator)effects; - - using var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Act + Assert - await Assert.ThrowsAnyAsync( - () => coordinator.PrepareForDatabaseRemovalAsync(snapshot, cts.Token)); - - Assert.Empty(snapshot.Items); - } - - [Fact] - public async Task PrepareForDatabaseRemovalAsync_WhenNoActiveLogs_ReturnsImmediatelyAndDispatchesNothing() - { - // Arrange — empty ActiveLogs is the "user removed a disabled entry" path. - var (effects, _, _, _, _) = CreateEffectsWithServices(); - var snapshot = new LogReopenSnapshot(); - - var coordinator = (ILogReloadCoordinator)effects; - - // Act - await coordinator.PrepareForDatabaseRemovalAsync(snapshot, TestContext.Current.CancellationToken); - - // Assert - Assert.Empty(snapshot.Items); - } - [Fact] public void ReopenAfterDatabaseRemoval_DispatchesOpenLogPerSnapshotEntry() { @@ -1341,13 +1261,13 @@ public void ReopenAfterDatabaseRemoval_DispatchesOpenLogPerSnapshotEntry() coordinator.ReopenAfterDatabaseRemoval(snapshot); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogName == Constants.LogNameLog1 && a.LogPathType == LogPathType.Channel)); - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.LogName == Constants.LogNameLog2 && a.LogPathType == LogPathType.File)); } - private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffects( + private static (Effects effects, IDispatcher mockDispatcher) CreateEffects( bool continuouslyUpdate = false, ImmutableDictionary? activeLogs = null, List? newEventBuffer = null, @@ -1403,7 +1323,7 @@ private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffec var mockDispatcher = Substitute.For(); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, mockFilterService, mockLogger, @@ -1418,7 +1338,7 @@ private static (EventLogEffects effects, IDispatcher mockDispatcher) CreateEffec return (effects, mockDispatcher); } - private static (EventLogEffects effects, + private static (Effects effects, IDispatcher mockDispatcher, IServiceProvider mockServiceProvider, IBannerService mockBannerService, @@ -1454,7 +1374,7 @@ private static (EventLogEffects effects, var mockDispatcher = Substitute.For(); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, Substitute.For(), Substitute.For(), @@ -1469,7 +1389,7 @@ private static (EventLogEffects effects, return (effects, mockDispatcher, mockServiceProvider, mockBannerService, mockDatabaseService); } - private static (EventLogEffects effects, + private static (Effects effects, IDispatcher mockDispatcher, IFilterService mockFilterService) CreateEffectsWithMutableState(Func stateProvider) { @@ -1500,7 +1420,7 @@ private static (EventLogEffects effects, var mockDispatcher = Substitute.For(); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, mockFilterService, mockLogger, @@ -1515,7 +1435,7 @@ private static (EventLogEffects effects, return (effects, mockDispatcher, mockFilterService); } - private static (EventLogEffects effects, + private static (Effects effects, IDispatcher mockDispatcher, ILogWatcherService mockLogWatcher, IEventResolverCache mockResolverCache, @@ -1560,7 +1480,7 @@ private static (EventLogEffects effects, var mockDispatcher = Substitute.For(); - var effects = new EventLogEffects( + var effects = new Effects( mockEventLogState, mockFilterService, mockLogger, diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs index 1bac89f8..ca8b3af4 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EventLogStoreTests.cs @@ -26,119 +26,25 @@ public void BufferedEventsWorkflow_ShouldHandleCorrectly() }; // Act - Buffer events - state = EventLogReducers.ReduceAddEventBuffered(state, new EventLogAction.AddEventBuffered(events, false)); + state = Reducers.ReduceAddEventBuffered(state, new AddEventBufferedAction(events, false)); // Assert Assert.Equal(2, state.NewEventBuffer.Count); Assert.False(state.NewEventBufferIsFull); // Act - Set continuously update - state = EventLogReducers.ReduceSetContinuouslyUpdate(state, new EventLogAction.SetContinuouslyUpdate(true)); + state = Reducers.ReduceSetContinuouslyUpdate(state, new SetContinuouslyUpdateAction(true)); // Assert Assert.True(state.ContinuouslyUpdate); } - [Fact] - public void EventLogAction_AddEventBuffered_ShouldStoreBufferAndFullFlag() - { - // Arrange - var events = new List - { - EventUtils.CreateTestEvent(100), - EventUtils.CreateTestEvent(200) - }; - - // Act - var action = new EventLogAction.AddEventBuffered(events, true); - - // Assert - Assert.Equal(2, action.UpdatedBuffer.Count); - Assert.True(action.IsFull); - } - - [Fact] - public void EventLogAction_AddEventSuccess_ShouldStoreActiveLogs() - { - // Arrange - var logData = new EventLogData("TestLog", LogPathType.Channel, []); - var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); - - // Act - var action = new EventLogAction.AddEventSuccess(activeLogs); - - // Assert - Assert.Single(action.ActiveLogs); - Assert.True(action.ActiveLogs.ContainsKey(Constants.LogNameTestLog)); - } - - [Fact] - public void EventLogAction_AddEvent_ShouldStoreNewEvent() - { - // Arrange - var newEvent = EventUtils.CreateTestEvent(100); - - // Act - var action = new EventLogAction.AddEvent(newEvent); - - // Assert - Assert.Equal(newEvent, action.NewEvent); - } - - [Fact] - public void EventLogAction_CloseLog_ShouldStoreLogIdAndName() - { - // Arrange - var logId = EventLogId.Create(); - var logName = Constants.LogNameTestLog; - - // Act - var action = new EventLogAction.CloseLog(logId, logName); - - // Assert - Assert.Equal(logId, action.LogId); - Assert.Equal(logName, action.LogName); - } - - [Fact] - public void EventLogAction_LoadEvents_ShouldStoreLogDataAndEvents() - { - // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - - var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200)); - - // Act - var action = new EventLogAction.LoadEvents(logData, events); - - // Assert - Assert.Equal(logData, action.LogData); - Assert.Equal(2, action.Events.Count); - } - - [Fact] - public void EventLogAction_OpenLog_ShouldStoreLogNameAndPathType() - { - // Arrange - var logName = Constants.LogNameApplication; - var pathType = LogPathType.Channel; - - // Act - var action = new EventLogAction.OpenLog(logName, pathType); - - // Assert - Assert.Equal(logName, action.LogName); - Assert.Equal(pathType, action.LogPathType); - } - [Fact] public void EventLogAction_OpenLog_WithCancellationToken_ShouldStoreToken() { // Arrange var cts = new CancellationTokenSource(); - - // Act - var action = new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.File, cts.Token); + var action = new OpenLogAction(Constants.LogNameTestLog, LogPathType.File, cts.Token); // Assert Assert.Equal(cts.Token, action.Token); @@ -151,7 +57,7 @@ public void EventLogAction_SelectEvent_DefaultFlags_ShouldBeFalse() var selectedEvent = EventUtils.CreateTestEvent(100); // Act - var action = new EventLogAction.SelectEvent(selectedEvent); + var action = new SelectEventAction(selectedEvent); // Assert Assert.False(action.IsMultiSelect); @@ -165,7 +71,7 @@ public void EventLogAction_SelectEvent_ShouldStoreEventAndSelectionFlags() var selectedEvent = EventUtils.CreateTestEvent(100); // Act - var action = new EventLogAction.SelectEvent(selectedEvent, true, true); + var action = new SelectEventAction(selectedEvent, true, true); // Assert Assert.Equal(selectedEvent, action.SelectedEvent); @@ -184,7 +90,7 @@ public void EventLogAction_SelectEvents_ShouldStoreMultipleEvents() }; // Act - var action = new EventLogAction.SelectEvents(events); + var action = new SelectEventsAction(events); // Assert Assert.Equal(2, action.SelectedEvents.Count); @@ -194,8 +100,8 @@ public void EventLogAction_SelectEvents_ShouldStoreMultipleEvents() public void EventLogAction_SetContinuouslyUpdate_ShouldStoreFlag() { // Act - var actionTrue = new EventLogAction.SetContinuouslyUpdate(true); - var actionFalse = new EventLogAction.SetContinuouslyUpdate(false); + var actionTrue = new SetContinuouslyUpdateAction(true); + var actionFalse = new SetContinuouslyUpdateAction(false); // Assert Assert.True(actionTrue.ContinuouslyUpdate); @@ -209,7 +115,7 @@ public void EventLogAction_SetFilters_ShouldStoreEventFilter() var eventFilter = new EventFilter(null, []); // Act - var action = new EventLogAction.SetFilters(eventFilter); + var action = new SetFiltersAction(eventFilter); // Assert Assert.Equal(eventFilter, action.EventFilter); @@ -343,8 +249,8 @@ public void LoadEventsAndSelect_ShouldWorkCorrectly() // Arrange var state = new EventLogState(); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel)); var logData = state.ActiveLogs[Constants.LogNameTestLog]; @@ -355,13 +261,13 @@ public void LoadEventsAndSelect_ShouldWorkCorrectly() ); // Act - Load events - state = EventLogReducers.ReduceLoadEvents(state, new EventLogAction.LoadEvents(logData, events)); + state = Reducers.ReduceLoadEvents(state, new LoadEventsAction(logData, events)); // Assert Assert.Equal(3, state.ActiveLogs[Constants.LogNameTestLog].Events.Count); // Act - Select event - state = EventLogReducers.ReduceSelectEvent(state, new EventLogAction.SelectEvent(events[0])); + state = Reducers.ReduceSelectEvent(state, new SelectEventAction(events[0])); // Assert Assert.Single(state.SelectedEvents); @@ -375,21 +281,21 @@ public void MultipleLogOperations_ShouldMaintainCorrectState() var state = new EventLogState(); // Act - Open multiple logs - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameLog1, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameLog1, LogPathType.Channel)); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameLog2, LogPathType.File)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameLog2, LogPathType.File)); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameLog3, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameLog3, LogPathType.Channel)); // Assert Assert.Equal(3, state.ActiveLogs.Count); // Act - Close one log var log2Id = state.ActiveLogs[Constants.LogNameLog2].Id; - state = EventLogReducers.ReduceCloseLog(state, new EventLogAction.CloseLog(log2Id, Constants.LogNameLog2)); + state = Reducers.ReduceCloseLog(state, new CloseLogAction(log2Id, Constants.LogNameLog2)); // Assert Assert.Equal(2, state.ActiveLogs.Count); @@ -405,15 +311,15 @@ public void OpenAndCloseLog_ShouldMaintainStateConsistency() var state = new EventLogState(); // Act - Open log - var openAction = new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel); - state = EventLogReducers.ReduceOpenLog(state, openAction); + var openAction = new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel); + state = Reducers.ReduceOpenLog(state, openAction); Assert.Single(state.ActiveLogs); // Act - Close log var logId = state.ActiveLogs[Constants.LogNameTestLog].Id; - var closeAction = new EventLogAction.CloseLog(logId, Constants.LogNameTestLog); - state = EventLogReducers.ReduceCloseLog(state, closeAction); + var closeAction = new CloseLogAction(logId, Constants.LogNameTestLog); + state = Reducers.ReduceCloseLog(state, closeAction); // Assert Assert.Empty(state.ActiveLogs); @@ -431,10 +337,10 @@ public void ReduceAddEventBuffered_ShouldUpdateBufferAndFullFlag() EventUtils.CreateTestEvent(200) }; - var action = new EventLogAction.AddEventBuffered(events, true); + var action = new AddEventBufferedAction(events, true); // Act - var newState = EventLogReducers.ReduceAddEventBuffered(state, action); + var newState = Reducers.ReduceAddEventBuffered(state, action); // Assert Assert.Equal(2, newState.NewEventBuffer.Count); @@ -447,10 +353,10 @@ public void ReduceAddEventBuffered_WhenNotFull_ShouldSetFullFlagFalse() // Arrange var state = new EventLogState { NewEventBufferIsFull = true }; var events = new List { EventUtils.CreateTestEvent(100) }; - var action = new EventLogAction.AddEventBuffered(events, false); + var action = new AddEventBufferedAction(events, false); // Act - var newState = EventLogReducers.ReduceAddEventBuffered(state, action); + var newState = Reducers.ReduceAddEventBuffered(state, action); // Assert Assert.False(newState.NewEventBufferIsFull); @@ -463,10 +369,10 @@ public void ReduceAddEventSuccess_ShouldUpdateActiveLogs() var state = new EventLogState(); var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); - var action = new EventLogAction.AddEventSuccess(activeLogs); + var action = new AddEventSuccessAction(activeLogs); // Act - var newState = EventLogReducers.ReduceAddEventSuccess(state, action); + var newState = Reducers.ReduceAddEventSuccess(state, action); // Assert Assert.Single(newState.ActiveLogs); @@ -488,7 +394,7 @@ public void ReduceCloseAll_ShouldClearAllState() }; // Act - var newState = EventLogReducers.ReduceCloseAll(state); + var newState = Reducers.ReduceCloseAll(state); // Assert Assert.Empty(newState.ActiveLogs); @@ -505,7 +411,7 @@ public void ReduceCloseAll_ShouldClearSelectedEvent() var state = new EventLogState { SelectedEvents = [first], SelectedEvent = first }; // Act - var newState = EventLogReducers.ReduceCloseAll(state); + var newState = Reducers.ReduceCloseAll(state); // Assert Assert.Null(newState.SelectedEvent); @@ -537,10 +443,10 @@ public void ReduceCloseLog_ShouldFilterNewEventBuffer() NewEventBuffer = [eventForLog1, eventForLog2] }; - var action = new EventLogAction.CloseLog(logData1.Id, Constants.LogNameLog1); + var action = new CloseLogAction(logData1.Id, Constants.LogNameLog1); // Act - var newState = EventLogReducers.ReduceCloseLog(state, action); + var newState = Reducers.ReduceCloseLog(state, action); // Assert Assert.Single(newState.NewEventBuffer); @@ -561,10 +467,10 @@ public void ReduceCloseLog_ShouldRemoveSpecifiedLog() .Add(Constants.LogNameLog2, logData2) }; - var action = new EventLogAction.CloseLog(logData1.Id, Constants.LogNameLog1); + var action = new CloseLogAction(logData1.Id, Constants.LogNameLog1); // Act - var newState = EventLogReducers.ReduceCloseLog(state, action); + var newState = Reducers.ReduceCloseLog(state, action); // Assert Assert.Single(newState.ActiveLogs); @@ -578,15 +484,15 @@ public void ReduceLoadEventsPartial_WhenLogIdDoesNotMatch_ShouldReturnStateUncha // Arrange var state = new EventLogState(); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel)); var staleLogData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act — stale partial with mismatched ID - var newState = EventLogReducers.ReduceLoadEventsPartial(state, - new EventLogAction.LoadEventsPartial(staleLogData, events)); + var newState = Reducers.ReduceLoadEventsPartial(state, + new LoadEventsPartialAction(staleLogData, events)); // Assert — state unchanged, original log preserved with its ID var originalId = state.ActiveLogs[Constants.LogNameTestLog].Id; @@ -604,8 +510,8 @@ public void ReduceLoadEventsPartial_WhenLogNotInActiveLogs_ShouldReturnStateUnch var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act - var newState = EventLogReducers.ReduceLoadEventsPartial(state, - new EventLogAction.LoadEventsPartial(logData, events)); + var newState = Reducers.ReduceLoadEventsPartial(state, + new LoadEventsPartialAction(logData, events)); // Assert Assert.Same(state, newState); @@ -624,10 +530,10 @@ public void ReduceLoadEvents_ShouldIsolateStateFromOriginalList() var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200)); - var action = new EventLogAction.LoadEvents(logData, events); + var action = new LoadEventsAction(logData, events); // Act - var newState = EventLogReducers.ReduceLoadEvents(state, action); + var newState = Reducers.ReduceLoadEvents(state, action); // ImmutableArray is inherently isolated — creating a new one doesn't affect the state var extendedEvents = events.Add(EventUtils.CreateTestEvent(300)); @@ -650,10 +556,10 @@ public void ReduceLoadEvents_ShouldUpdateLogWithEvents() var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100), EventUtils.CreateTestEvent(200)); - var action = new EventLogAction.LoadEvents(logData, events); + var action = new LoadEventsAction(logData, events); // Act - var newState = EventLogReducers.ReduceLoadEvents(state, action); + var newState = Reducers.ReduceLoadEvents(state, action); // Assert Assert.Equal(2, newState.ActiveLogs[Constants.LogNameTestLog].Events.Count); @@ -665,15 +571,15 @@ public void ReduceLoadEvents_WhenLogIdDoesNotMatch_ShouldReturnStateUnchanged() // Arrange — open a log, then create stale logData with a different ID var state = new EventLogState(); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel)); // Create stale logData with a new ID (simulating a previous load instance) var staleLogData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act — stale LoadEvents with mismatched ID - var newState = EventLogReducers.ReduceLoadEvents(state, new EventLogAction.LoadEvents(staleLogData, events)); + var newState = Reducers.ReduceLoadEvents(state, new LoadEventsAction(staleLogData, events)); // Assert — state unchanged, original log preserved with its ID and empty events var originalId = state.ActiveLogs[Constants.LogNameTestLog].Id; @@ -688,14 +594,14 @@ public void ReduceLoadEvents_WhenLogIdMatches_ShouldUpdateLog() // Arrange var state = new EventLogState(); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel)); var logData = state.ActiveLogs[Constants.LogNameTestLog]; var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act — LoadEvents with matching ID - var newState = EventLogReducers.ReduceLoadEvents(state, new EventLogAction.LoadEvents(logData, events)); + var newState = Reducers.ReduceLoadEvents(state, new LoadEventsAction(logData, events)); // Assert — events applied Assert.Single(newState.ActiveLogs[Constants.LogNameTestLog].Events); @@ -710,7 +616,7 @@ public void ReduceLoadEvents_WhenLogNotInActiveLogs_ShouldReturnStateUnchanged() var events = ImmutableArray.Create(EventUtils.CreateTestEvent(100)); // Act — stale LoadEvents arrives for a closed log - var newState = EventLogReducers.ReduceLoadEvents(state, new EventLogAction.LoadEvents(logData, events)); + var newState = Reducers.ReduceLoadEvents(state, new LoadEventsAction(logData, events)); // Assert — state unchanged, log NOT resurrected Assert.Same(state, newState); @@ -722,10 +628,10 @@ public void ReduceOpenLog_ShouldAddEmptyLogData() { // Arrange var state = new EventLogState(); - var action = new EventLogAction.OpenLog(Constants.LogNameNewLog, LogPathType.Channel); + var action = new OpenLogAction(Constants.LogNameNewLog, LogPathType.Channel); // Act - var newState = EventLogReducers.ReduceOpenLog(state, action); + var newState = Reducers.ReduceOpenLog(state, action); // Assert Assert.Single(newState.ActiveLogs); @@ -740,14 +646,14 @@ public void ReduceOpenLog_WhenLogAlreadyActive_ShouldReturnSameStateInstance() // active log (drag/drop, command line, recent menu) cannot replace its EventLogData // (which would invalidate ongoing loads and reset the user's events). var state = new EventLogState(); - state = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + state = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel)); var existingLogData = state.ActiveLogs[Constants.LogNameTestLog]; // Act — dispatch the same OpenLog a second time. - var newState = EventLogReducers.ReduceOpenLog(state, - new EventLogAction.OpenLog(Constants.LogNameTestLog, LogPathType.Channel)); + var newState = Reducers.ReduceOpenLog(state, + new OpenLogAction(Constants.LogNameTestLog, LogPathType.Channel)); // Assert — same state reference, same EventLogData reference (no replacement). Assert.Same(state, newState); @@ -760,10 +666,10 @@ public void ReduceOpenLog_WithFilePath_ShouldSetCorrectPathType() { // Arrange var state = new EventLogState(); - var action = new EventLogAction.OpenLog(Constants.FilePathTestEvtx, LogPathType.File); + var action = new OpenLogAction(Constants.FilePathTestEvtx, LogPathType.File); // Act - var newState = EventLogReducers.ReduceOpenLog(state, action); + var newState = Reducers.ReduceOpenLog(state, action); // Assert Assert.Equal(LogPathType.File, newState.ActiveLogs[Constants.FilePathTestEvtx].Type); @@ -776,10 +682,10 @@ public void ReduceSelectEvent_OnToggleOff_ShouldKeepSelectedEvent() var first = EventUtils.CreateTestEvent(100); var second = EventUtils.CreateTestEvent(200); var state = new EventLogState { SelectedEvents = [first, second], SelectedEvent = first }; - var action = new EventLogAction.SelectEvent(second, IsMultiSelect: true); + var action = new SelectEventAction(second, IsMultiSelect: true); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.DoesNotContain(second, newState.SelectedEvents); @@ -793,10 +699,10 @@ public void ReduceSelectEvent_ShouldSetSelectedEventOnAdd() var first = EventUtils.CreateTestEvent(100); var second = EventUtils.CreateTestEvent(200); var state = new EventLogState { SelectedEvents = [first], SelectedEvent = first }; - var action = new EventLogAction.SelectEvent(second, IsMultiSelect: true); + var action = new SelectEventAction(second, IsMultiSelect: true); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.Contains(second, newState.SelectedEvents); @@ -809,10 +715,10 @@ public void ReduceSelectEvent_WhenEventNotSelected_ShouldAddEvent() // Arrange var state = new EventLogState(); var selectedEvent = EventUtils.CreateTestEvent(100); - var action = new EventLogAction.SelectEvent(selectedEvent); + var action = new SelectEventAction(selectedEvent); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.Single(newState.SelectedEvents); @@ -825,10 +731,10 @@ public void ReduceSelectEvent_WhenMultiSelectAndEventAlreadySelected_ShouldRemov // Arrange var selectedEvent = EventUtils.CreateTestEvent(100); var state = new EventLogState { SelectedEvents = [selectedEvent] }; - var action = new EventLogAction.SelectEvent(selectedEvent, true); + var action = new SelectEventAction(selectedEvent, true); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.Empty(newState.SelectedEvents); @@ -841,10 +747,10 @@ public void ReduceSelectEvent_WhenMultiSelect_ShouldAddToExisting() var existingEvent = EventUtils.CreateTestEvent(100); var state = new EventLogState { SelectedEvents = [existingEvent] }; var newEvent = EventUtils.CreateTestEvent(200); - var action = new EventLogAction.SelectEvent(newEvent, true); + var action = new SelectEventAction(newEvent, true); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.Equal(2, newState.SelectedEvents.Count); @@ -859,10 +765,10 @@ public void ReduceSelectEvent_WhenNotMultiSelect_ShouldReplaceExisting() var existingEvent = EventUtils.CreateTestEvent(100); var state = new EventLogState { SelectedEvents = [existingEvent] }; var newEvent = EventUtils.CreateTestEvent(200); - var action = new EventLogAction.SelectEvent(newEvent); + var action = new SelectEventAction(newEvent); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.Single(newState.SelectedEvents); @@ -877,10 +783,10 @@ public void ReduceSelectEvent_WhenShouldStaySelected_ShouldOnlyUpdateSelectedEve // moves the focus cursor to that row but doesn't change the selection list. var selectedEvent = EventUtils.CreateTestEvent(100); var state = new EventLogState { SelectedEvents = [selectedEvent], SelectedEvent = null }; - var action = new EventLogAction.SelectEvent(selectedEvent, false, true); + var action = new SelectEventAction(selectedEvent, false, true); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert Assert.Same(state.SelectedEvents, newState.SelectedEvents); @@ -895,10 +801,10 @@ public void ReduceSelectEvent_WhenValueEqualButDifferentReference_ShouldNotMatch var staleReference = EventUtils.CreateTestEvent(100, recordId: 5); var freshReference = EventUtils.CreateTestEvent(100, recordId: 5); var state = new EventLogState { SelectedEvents = [staleReference] }; - var action = new EventLogAction.SelectEvent(freshReference, IsMultiSelect: true); + var action = new SelectEventAction(freshReference, IsMultiSelect: true); // Act - var newState = EventLogReducers.ReduceSelectEvent(state, action); + var newState = Reducers.ReduceSelectEvent(state, action); // Assert — fresh reference should be added (not treated as already // selected), giving us two entries. @@ -920,10 +826,10 @@ public void ReduceSelectEvents_ShouldAddNewEventsOnly() EventUtils.CreateTestEvent(200) // New }; - var action = new EventLogAction.SelectEvents(newEvents); + var action = new SelectEventsAction(newEvents); // Act - var newState = EventLogReducers.ReduceSelectEvents(state, action); + var newState = Reducers.ReduceSelectEvents(state, action); // Assert Assert.Equal(2, newState.SelectedEvents.Count); @@ -937,10 +843,10 @@ public void ReduceSelectEvents_ShouldPreserveSelectedEventIfStillPresent() var second = EventUtils.CreateTestEvent(200); var third = EventUtils.CreateTestEvent(300); var state = new EventLogState { SelectedEvents = [first, second], SelectedEvent = second }; - var action = new EventLogAction.SelectEvents([second, third]); + var action = new SelectEventsAction([second, third]); // Act - var newState = EventLogReducers.ReduceSelectEvents(state, action); + var newState = Reducers.ReduceSelectEvents(state, action); // Assert — active was already in incoming so it stays. Assert.Same(second, newState.SelectedEvent); @@ -952,10 +858,10 @@ public void ReduceSelectEvents_WhenAllEventsAlreadySelected_ShouldNotAddDuplicat // Arrange var existingEvent = EventUtils.CreateTestEvent(100); var state = new EventLogState { SelectedEvents = [existingEvent] }; - var action = new EventLogAction.SelectEvents([existingEvent]); + var action = new SelectEventsAction([existingEvent]); // Act - var newState = EventLogReducers.ReduceSelectEvents(state, action); + var newState = Reducers.ReduceSelectEvents(state, action); // Assert Assert.Single(newState.SelectedEvents); @@ -970,10 +876,10 @@ public void ReduceSelectEvents_WhenSelectedEventDropped_ShouldFallbackToLastInco var second = EventUtils.CreateTestEvent(200); var third = EventUtils.CreateTestEvent(300); var state = new EventLogState { SelectedEvents = [], SelectedEvent = null }; - var action = new EventLogAction.SelectEvents([second, third]); + var action = new SelectEventsAction([second, third]); // Act - var newState = EventLogReducers.ReduceSelectEvents(state, action); + var newState = Reducers.ReduceSelectEvents(state, action); // Assert Assert.Same(third, newState.SelectedEvent); @@ -984,10 +890,10 @@ public void ReduceSetContinuouslyUpdate_ShouldSetFlag() { // Arrange var state = new EventLogState { ContinuouslyUpdate = false }; - var action = new EventLogAction.SetContinuouslyUpdate(true); + var action = new SetContinuouslyUpdateAction(true); // Act - var newState = EventLogReducers.ReduceSetContinuouslyUpdate(state, action); + var newState = Reducers.ReduceSetContinuouslyUpdate(state, action); // Assert Assert.True(newState.ContinuouslyUpdate); @@ -1003,10 +909,10 @@ public void ReduceSetFilters_WhenFilterChanged_ShouldUpdateFilter() var before = new DateTime(2024, 1, 2, 12, 0, 0, DateTimeKind.Utc); var newFilter = new EventFilter(new FilterDateModel { After = after, Before = before }, []); - var action = new EventLogAction.SetFilters(newFilter); + var action = new SetFiltersAction(newFilter); // Act - var newState = EventLogReducers.ReduceSetFilters(state, action); + var newState = Reducers.ReduceSetFilters(state, action); // Assert Assert.Equal(newFilter, newState.AppliedFilter); @@ -1018,10 +924,10 @@ public void ReduceSetFilters_WhenFilterUnchanged_ShouldReturnSameState() // Arrange var filter = new EventFilter(null, []); var state = new EventLogState { AppliedFilter = filter }; - var action = new EventLogAction.SetFilters(filter); + var action = new SetFiltersAction(filter); // Act - var newState = EventLogReducers.ReduceSetFilters(state, action); + var newState = Reducers.ReduceSetFilters(state, action); // Assert Assert.Same(state, newState); @@ -1036,10 +942,10 @@ public void ReduceSetSelectedEvents_ShouldReplaceSelectionPreservingOrder() var first = EventUtils.CreateTestEvent(200); var second = EventUtils.CreateTestEvent(300); var third = EventUtils.CreateTestEvent(400); - var action = new EventLogAction.SetSelectedEvents([first, second, third], third); + var action = new SetSelectedEventsAction([first, second, third], third); // Act - var newState = EventLogReducers.ReduceSetSelectedEvents(state, action); + var newState = Reducers.ReduceSetSelectedEvents(state, action); // Assert — replaces existingEvent and preserves the caller-provided // selection order. The active/focused event is tracked separately via @@ -1058,10 +964,10 @@ public void ReduceSetSelectedEvents_ShouldSetSelectedEventIndependentOfMembershi var second = EventUtils.CreateTestEvent(200); var notInSelection = EventUtils.CreateTestEvent(300); var state = new EventLogState(); - var action = new EventLogAction.SetSelectedEvents([first, second], notInSelection); + var action = new SetSelectedEventsAction([first, second], notInSelection); // Act - var newState = EventLogReducers.ReduceSetSelectedEvents(state, action); + var newState = Reducers.ReduceSetSelectedEvents(state, action); // Assert Assert.Equal([first, second], newState.SelectedEvents); @@ -1075,10 +981,10 @@ public void ReduceSetSelectedEvents_WhenInputContainsDuplicates_ShouldDistinctBy var first = EventUtils.CreateTestEvent(100); var second = EventUtils.CreateTestEvent(200); var state = new EventLogState(); - var action = new EventLogAction.SetSelectedEvents([first, second, first], first); + var action = new SetSelectedEventsAction([first, second, first], first); // Act - var newState = EventLogReducers.ReduceSetSelectedEvents(state, action); + var newState = Reducers.ReduceSetSelectedEvents(state, action); // Assert Assert.Equal(2, newState.SelectedEvents.Count); @@ -1091,10 +997,10 @@ public void ReduceSetSelectedEvents_WhenInputIsEmpty_ShouldClearSelection() { // Arrange var state = new EventLogState { SelectedEvents = [EventUtils.CreateTestEvent(100)] }; - var action = new EventLogAction.SetSelectedEvents([], null); + var action = new SetSelectedEventsAction([], null); // Act - var newState = EventLogReducers.ReduceSetSelectedEvents(state, action); + var newState = Reducers.ReduceSetSelectedEvents(state, action); // Assert Assert.Empty(newState.SelectedEvents); @@ -1107,10 +1013,10 @@ public void ReduceSetSelectedEvents_WhenOnlySelectedEventChanged_ShouldUpdateOnl var first = EventUtils.CreateTestEvent(100); var second = EventUtils.CreateTestEvent(200); var state = new EventLogState { SelectedEvents = [first, second], SelectedEvent = first }; - var action = new EventLogAction.SetSelectedEvents([first, second], second); + var action = new SetSelectedEventsAction([first, second], second); // Act - var newState = EventLogReducers.ReduceSetSelectedEvents(state, action); + var newState = Reducers.ReduceSetSelectedEvents(state, action); // Assert — selection list is preserved by reference (no churn) but active changed. Assert.Same(state.SelectedEvents, newState.SelectedEvents); @@ -1126,10 +1032,10 @@ public void ReduceSetSelectedEvents_WhenSelectionUnchanged_ShouldReturnSameState var first = EventUtils.CreateTestEvent(100); var second = EventUtils.CreateTestEvent(200); var state = new EventLogState { SelectedEvents = [first, second], SelectedEvent = second }; - var action = new EventLogAction.SetSelectedEvents([first, second], second); + var action = new SetSelectedEventsAction([first, second], second); // Act - var newState = EventLogReducers.ReduceSetSelectedEvents(state, action); + var newState = Reducers.ReduceSetSelectedEvents(state, action); // Assert Assert.Same(state, newState); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs similarity index 85% rename from tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableEffectsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs index ba320306..27fe3f6d 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs @@ -9,7 +9,7 @@ namespace EventLogExpert.UI.Tests.Store.EventTable; -public sealed class EventTableEffectsTests +public sealed class EffectsTests { [Fact] public async Task HandleLoadColumns_ShouldLoadAllColumnsFromPreferences() @@ -28,7 +28,7 @@ public async Task HandleLoadColumns_ShouldLoadAllColumnsFromPreferences() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns.Count == Enum.GetValues().Length)); } @@ -49,7 +49,7 @@ public async Task HandleLoadColumns_ShouldLoadWidthsFromPreferences() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.ColumnWidths[ColumnName.Level] == 150)); } @@ -68,7 +68,7 @@ public async Task HandleLoadColumns_ShouldMarkDisabledColumnsAsFalse() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns[ColumnName.Source] == false && action.LoadedColumns[ColumnName.EventId] == false)); } @@ -89,7 +89,7 @@ public async Task HandleLoadColumns_ShouldMarkEnabledColumnsAsTrue() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns[ColumnName.Level] == true && action.LoadedColumns[ColumnName.DateAndTime] == true)); } @@ -106,7 +106,7 @@ public async Task HandleLoadColumns_ShouldUseDefaultOrderWhenNotSaved() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.ColumnOrder.SequenceEqual(ColumnDefaults.Order))); } @@ -122,7 +122,7 @@ public async Task HandleLoadColumns_ShouldUseDefaultWidthsWhenNotSaved() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.ColumnWidths[ColumnName.Level] == ColumnDefaults.GetWidth(ColumnName.Level))); } @@ -140,7 +140,7 @@ public async Task HandleLoadColumns_ShouldUseSavedOrderWhenPresent() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.ColumnOrder[0] == ColumnName.Source && action.ColumnOrder[1] == ColumnName.Level)); } @@ -157,7 +157,7 @@ public async Task HandleLoadColumns_WhenNoColumnsEnabled_ShouldMarkAllAsFalse() await effects.HandleLoadColumns(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns.All(kvp => kvp.Value == false))); } @@ -172,7 +172,7 @@ public async Task HandleReorderColumn_ShouldPersistToPreferences() var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(state: postReducerState); - var action = new EventTableAction.ReorderColumn(ColumnName.Source, ColumnName.Level, false); + var action = new ReorderColumnAction(ColumnName.Source, ColumnName.Level, false); // Act await effects.HandleReorderColumn(action, mockDispatcher); @@ -202,7 +202,7 @@ public async Task HandleResetColumnDefaults_ShouldResetAllColumnSettingsToDefaul var expectedWidths = ColumnDefaults.Widths.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.ColumnWidths.Count == expectedWidths.Count && action.ColumnWidths.All(kvp => expectedWidths.ContainsKey(kvp.Key) && expectedWidths[kvp.Key] == kvp.Value) && @@ -229,7 +229,7 @@ public async Task HandleSetColumnWidth_ShouldPersistToPreferences() var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(state: postReducerState); - var action = new EventTableAction.SetColumnWidth(ColumnName.Level, 200); + var action = new SetColumnWidthAction(ColumnName.Level, 200); // Act await effects.HandleSetColumnWidth(action, mockDispatcher); @@ -252,13 +252,13 @@ public async Task HandleToggleColumn_ShouldOnlyChangeToggledColumn() }; var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(enabledColumns); - var action = new EventTableAction.ToggleColumn(ColumnName.DateAndTime); + var action = new ToggleColumnAction(ColumnName.DateAndTime); // Act await effects.HandleToggleColumn(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns[ColumnName.Level] == true && action.LoadedColumns[ColumnName.DateAndTime] == false && action.LoadedColumns[ColumnName.Source] == true && @@ -276,7 +276,7 @@ public async Task HandleToggleColumn_ShouldUpdatePreferences() }; var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(enabledColumns); - var action = new EventTableAction.ToggleColumn(ColumnName.Source); + var action = new ToggleColumnAction(ColumnName.Source); // Act await effects.HandleToggleColumn(action, mockDispatcher); @@ -298,13 +298,13 @@ public async Task HandleToggleColumn_WhenColumnDisabled_ShouldEnableIt() }; var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(enabledColumns); - var action = new EventTableAction.ToggleColumn(ColumnName.Level); + var action = new ToggleColumnAction(ColumnName.Level); // Act await effects.HandleToggleColumn(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns[ColumnName.Level] == true && action.LoadedColumns[ColumnName.DateAndTime] == true && action.LoadedColumns[ColumnName.Source] == true)); @@ -322,13 +322,13 @@ public async Task HandleToggleColumn_WhenColumnEnabled_ShouldDisableIt() }; var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(enabledColumns); - var action = new EventTableAction.ToggleColumn(ColumnName.Level); + var action = new ToggleColumnAction(ColumnName.Level); // Act await effects.HandleToggleColumn(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(action => + mockDispatcher.Received(1).Dispatch(Arg.Is(action => action.LoadedColumns[ColumnName.Level] == false && action.LoadedColumns[ColumnName.DateAndTime] == true && action.LoadedColumns[ColumnName.Source] == true)); @@ -346,7 +346,7 @@ public async Task HandleToggleColumn_WhenDisabling_ShouldRemoveFromPreferences() }; var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(enabledColumns); - var action = new EventTableAction.ToggleColumn(ColumnName.Source); + var action = new ToggleColumnAction(ColumnName.Source); // Act await effects.HandleToggleColumn(action, mockDispatcher); @@ -370,7 +370,7 @@ public async Task HandleToggleColumn_WhenEnabling_ShouldPersistToPreferences() }; var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(enabledColumns); - var action = new EventTableAction.ToggleColumn(ColumnName.Source); + var action = new ToggleColumnAction(ColumnName.Source); // Act await effects.HandleToggleColumn(action, mockDispatcher); @@ -383,7 +383,7 @@ public async Task HandleToggleColumn_WhenEnabling_ShouldPersistToPreferences() columns.Count() == 2); } - private static (EventTableEffects effects, IDispatcher mockDispatcher, IPreferencesProvider mockPreferencesProvider) + private static (Effects effects, IDispatcher mockDispatcher, IPreferencesProvider mockPreferencesProvider) CreateEffects(List? enabledColumns = null, EventTableState? state = null) { var mockPreferencesProvider = Substitute.For(); @@ -394,7 +394,7 @@ private static (EventTableEffects effects, IDispatcher mockDispatcher, IPreferen var mockState = Substitute.For>(); mockState.Value.Returns(state ?? new EventTableState()); - var effects = new EventTableEffects(mockPreferencesProvider, mockState); + var effects = new Effects(mockPreferencesProvider, mockState); var mockDispatcher = Substitute.For(); return (effects, mockDispatcher, mockPreferencesProvider); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs index 70bcdb87..47a224b4 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EventTableStoreTests.cs @@ -7,7 +7,6 @@ using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; -using System.Collections.Immutable; namespace EventLogExpert.UI.Tests.Store.EventTable; @@ -20,7 +19,7 @@ public void EventTableAction_AddTable_ShouldStoreLogData() var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); // Act - var action = new EventTableAction.AddTable(logData); + var action = new AddTableAction(logData); // Assert Assert.Equal(logData, action.LogData); @@ -33,7 +32,7 @@ public void EventTableAction_CloseLog_ShouldStoreLogId() var logId = EventLogId.Create(); // Act - var action = new EventTableAction.CloseLog(logId); + var action = new CloseLogAction(logId); // Assert Assert.Equal(logId, action.LogId); @@ -58,7 +57,7 @@ public void EventTableAction_LoadColumnsCompleted_ShouldStoreColumns() var order = ColumnDefaults.Order; // Act - var action = new EventTableAction.LoadColumnsCompleted(columns, widths, order); + var action = new LoadColumnsCompletedAction(columns, widths, order); // Assert Assert.Equal(2, action.LoadedColumns.Count); @@ -73,7 +72,7 @@ public void EventTableAction_SetActiveTable_ShouldStoreLogId() var logId = EventLogId.Create(); // Act - var action = new EventTableAction.SetActiveTable(logId); + var action = new SetActiveTableAction(logId); // Assert Assert.Equal(logId, action.LogId); @@ -83,7 +82,7 @@ public void EventTableAction_SetActiveTable_ShouldStoreLogId() public void EventTableAction_SetOrderBy_ShouldStoreColumnName() { // Act - var action = new EventTableAction.SetOrderBy(ColumnName.Level); + var action = new SetOrderByAction(ColumnName.Level); // Assert Assert.Equal(ColumnName.Level, action.OrderBy); @@ -93,7 +92,7 @@ public void EventTableAction_SetOrderBy_ShouldStoreColumnName() public void EventTableAction_SetOrderBy_WithNull_ShouldStoreNull() { // Act - var action = new EventTableAction.SetOrderBy(null); + var action = new SetOrderByAction(null); // Assert Assert.Null(action.OrderBy); @@ -103,7 +102,7 @@ public void EventTableAction_SetOrderBy_WithNull_ShouldStoreNull() public void EventTableAction_ToggleColumn_ShouldStoreColumnName() { // Act - var action = new EventTableAction.ToggleColumn(ColumnName.Source); + var action = new ToggleColumnAction(ColumnName.Source); // Assert Assert.Equal(ColumnName.Source, action.ColumnName); @@ -116,7 +115,7 @@ public void EventTableAction_ToggleLoading_ShouldStoreLogId() var logId = EventLogId.Create(); // Act - var action = new EventTableAction.ToggleLoading(logId); + var action = new ToggleLoadingAction(logId); // Assert Assert.Equal(logId, action.LogId); @@ -135,7 +134,7 @@ public void EventTableAction_UpdateDisplayedEvents_ShouldStoreActiveLogs() }; // Act - var action = new EventTableAction.UpdateDisplayedEvents(activeLogs); + var action = new UpdateDisplayedEventsAction(activeLogs); // Assert Assert.Single(action.ActiveLogs); @@ -155,7 +154,7 @@ public void EventTableAction_UpdateTable_ShouldStoreLogIdAndEvents() }; // Act - var action = new EventTableAction.UpdateTable(logId, events); + var action = new UpdateTableAction(logId, events); // Assert Assert.Equal(logId, action.LogId); @@ -168,16 +167,16 @@ public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOn // Arrange var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var firstBatch = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, ComputerName = Constants.EventComputerServer01 } }; - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, firstBatch)); + new AppendTableEventsAction(logData.Id, firstBatch)); var secondBatch = new List { @@ -185,9 +184,9 @@ public void EventTableModel_ComputerName_AfterFirstEventArrives_ShouldBeStoredOn }; // Act — second batch with a different ComputerName must not overwrite the latched value - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, secondBatch)); + new AppendTableEventsAction(logData.Id, secondBatch)); // Assert — ComputerName latches to the first non-empty observed value var table = state.EventTables.First(t => t.Id == logData.Id); @@ -200,7 +199,7 @@ public void EventTableModel_ComputerName_WhenFirstEventHasEmptyComputerName_Shou // Arrange — first event has an empty ComputerName (resolver miss); second carries the name var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var batch = new List { @@ -209,9 +208,9 @@ public void EventTableModel_ComputerName_WhenFirstEventHasEmptyComputerName_Shou }; // Act - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, batch)); + new AppendTableEventsAction(logData.Id, batch)); // Assert — reducer scans past the empty leading event and latches the first non-empty value var table = state.EventTables.First(t => t.Id == logData.Id); @@ -272,9 +271,9 @@ public void IntegrationTest_ColumnManagement() { ColumnName.Source, false } }; - state = EventTableReducers.ReduceLoadColumnsCompleted( + state = Reducers.ReduceLoadColumnsCompleted( state, - new EventTableAction.LoadColumnsCompleted(columns, new Dictionary(), ColumnDefaults.Order)); + new LoadColumnsCompletedAction(columns, new Dictionary(), ColumnDefaults.Order)); // Assert Assert.Equal(3, state.Columns.Count); @@ -290,7 +289,7 @@ public void IntegrationTest_LoadAndUpdateTableEvents() var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); // Act - Add table - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); Assert.True(state.EventTables.First().IsLoading); // Act - Update table with events @@ -300,7 +299,7 @@ public void IntegrationTest_LoadAndUpdateTableEvents() EventUtils.CreateTestEvent(200) }; - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, events)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData.Id, events)); // Assert Assert.False(state.EventTables.First().IsLoading); @@ -317,16 +316,16 @@ public void IntegrationTest_OpenMultipleLogsAndCloseOne() var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); // Act - Open three logs - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData3)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData3)); // Assert - Should have 4 tables (3 logs + 1 combined) Assert.Equal(4, state.EventTables.Count); Assert.Single(state.EventTables, t => t.IsCombined); // Act - Close one log - state = EventTableReducers.ReduceCloseLog(state, new EventTableAction.CloseLog(logData2.Id)); + state = Reducers.ReduceCloseLog(state, new CloseLogAction(logData2.Id)); // Assert - Should have 3 tables (2 logs + 1 combined) Assert.Equal(3, state.EventTables.Count); @@ -341,14 +340,14 @@ public void ReduceAddTable_WhenCombinedExists_ShouldNotCreateAnotherCombined() var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); - var action = new EventTableAction.AddTable(logData3); + var action = new AddTableAction(logData3); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert Assert.Equal(4, newState.EventTables.Count); @@ -361,10 +360,10 @@ public void ReduceAddTable_WhenFirstTable_ShouldBeLoading() // Arrange var state = new EventTableState(); var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var action = new EventTableAction.AddTable(logData); + var action = new AddTableAction(logData); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert Assert.True(newState.EventTables.First().IsLoading); @@ -376,10 +375,10 @@ public void ReduceAddTable_WhenFirstTable_ShouldSetAsActive() // Arrange var state = new EventTableState(); var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var action = new EventTableAction.AddTable(logData); + var action = new AddTableAction(logData); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert Assert.Single(newState.EventTables); @@ -394,10 +393,10 @@ public void ReduceAddTable_WhenFirstTable_WithFilePath_ShouldSetFileName() // Arrange var state = new EventTableState(); var logData = new EventLogData(Constants.FilePathTestEvtx, LogPathType.File, []); - var action = new EventTableAction.AddTable(logData); + var action = new AddTableAction(logData); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert Assert.Equal(Constants.FilePathTestEvtx, newState.EventTables.First().FileName); @@ -410,10 +409,10 @@ public void ReduceAddTable_WhenFirstTable_WithLogName_ShouldNotSetFileName() // Arrange var state = new EventTableState(); var logData = new EventLogData(Constants.LogNameApplication, LogPathType.Channel, []); - var action = new EventTableAction.AddTable(logData); + var action = new AddTableAction(logData); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert Assert.Null(newState.EventTables.First().FileName); @@ -427,13 +426,13 @@ public void ReduceAddTable_WhenSecondTable_ShouldCreateCombinedTable() // Arrange var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var action = new EventTableAction.AddTable(logData2); + var action = new AddTableAction(logData2); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert Assert.Equal(3, newState.EventTables.Count); @@ -446,13 +445,13 @@ public void ReduceAddTable_WhenSecondTable_ShouldSetCombinedAsActive() // Arrange var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var action = new EventTableAction.AddTable(logData2); + var action = new AddTableAction(logData2); // Act - var newState = EventTableReducers.ReduceAddTable(state, action); + var newState = Reducers.ReduceAddTable(state, action); // Assert var combinedTable = newState.EventTables.First(t => t.IsCombined); @@ -465,19 +464,19 @@ public void ReduceAppendTableEventsBatch_WhenBatchTargetsClosedLog_ShouldSkipTha // Arrange — open log plus a stale log id whose tab no longer exists (race: closed mid-flight) var openLog = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); + state = Reducers.ReduceAddTable(state, new AddTableAction(openLog)); var staleLogId = EventLogId.Create(); var batches = new Dictionary> { - { openLog.Id, [new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }] }, - { staleLogId, [new("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 }] } + { openLog.Id, [new ResolvedEvent(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 }] }, + { staleLogId, [new ResolvedEvent("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 }] } }; // Act - var newState = EventTableReducers.ReduceAppendTableEventsBatch( + var newState = Reducers.ReduceAppendTableEventsBatch( state, - new EventTableAction.AppendTableEventsBatch(batches)); + new AppendTableEventsBatchAction(batches)); // Assert — stale batch is skipped; canonical and EventCountByLog only reflect the open log Assert.Single(newState.DisplayedEvents); @@ -492,7 +491,7 @@ public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents( // Arrange var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var initialEvents = new List { @@ -500,9 +499,9 @@ public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents( EventUtils.CreateTestEvent(200, recordId: 2) }; - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, initialEvents)); + new AppendTableEventsAction(logData.Id, initialEvents)); var deltaEvents = new List { @@ -510,10 +509,10 @@ public void ReduceAppendTableEvents_ShouldAppendEventsToExistingDisplayedEvents( EventUtils.CreateTestEvent(400, recordId: 4) }; - var action = new EventTableAction.AppendTableEvents(logData.Id, deltaEvents); + var action = new AppendTableEventsAction(logData.Id, deltaEvents); // Act - var newState = EventTableReducers.ReduceAppendTableEvents(state, action); + var newState = Reducers.ReduceAppendTableEvents(state, action); // Assert var updatedTable = newState.EventTables.First(t => t.Id == logData.Id); @@ -527,17 +526,17 @@ public void ReduceAppendTableEvents_ShouldNotChangeIsLoading() // Arrange var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); // Table should be in loading state after AddTable Assert.True(state.EventTables.First(t => t.Id == logData.Id).IsLoading); - var action = new EventTableAction.AppendTableEvents( + var action = new AppendTableEventsAction( logData.Id, new List { EventUtils.CreateTestEvent(100, recordId: 1) }); // Act - var newState = EventTableReducers.ReduceAppendTableEvents(state, action); + var newState = Reducers.ReduceAppendTableEvents(state, action); // Assert - IsLoading should still be true (partial update doesn't complete loading) var updatedTable = newState.EventTables.First(t => t.Id == logData.Id); @@ -551,7 +550,7 @@ public void ReduceAppendTableEvents_ShouldPreserveSortOrder() // Arrange var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var initialEvents = new List { @@ -559,9 +558,9 @@ public void ReduceAppendTableEvents_ShouldPreserveSortOrder() EventUtils.CreateTestEvent(200, recordId: 20) }; - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, initialEvents)); + new AppendTableEventsAction(logData.Id, initialEvents)); // Append events with record IDs that should sort between and after existing var deltaEvents = new List @@ -570,10 +569,10 @@ public void ReduceAppendTableEvents_ShouldPreserveSortOrder() EventUtils.CreateTestEvent(400, recordId: 15) }; - var action = new EventTableAction.AppendTableEvents(logData.Id, deltaEvents); + var action = new AppendTableEventsAction(logData.Id, deltaEvents); // Act - var newState = EventTableReducers.ReduceAppendTableEvents(state, action); + var newState = Reducers.ReduceAppendTableEvents(state, action); // Assert - sort is by DateAndTime descending; ties on TimeCreated fall through // to the RecordId tiebreaker, which preserves descending RecordId order here. @@ -590,620 +589,21 @@ public void ReduceAppendTableEvents_WhenTableNotFound_ShouldReturnUnchangedState // Arrange var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var unknownLogId = EventLogId.Create(); - var action = new EventTableAction.AppendTableEvents( + var action = new AppendTableEventsAction( unknownLogId, new List { EventUtils.CreateTestEvent(100) }); // Act - var newState = EventTableReducers.ReduceAppendTableEvents(state, action); + var newState = Reducers.ReduceAppendTableEvents(state, action); // Assert Assert.Same(state, newState); } - [Fact] - public void ReduceCloseAll_ShouldClearAllTables() - { - // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - - // Act - var newState = EventTableReducers.ReduceCloseAll(state); - - // Assert - Assert.Empty(newState.EventTables); - Assert.Null(newState.ActiveEventLogId); - } - - [Fact] - public void ReduceCloseLog_ShouldScrubCanonicalRowsAndCountForClosedLog() - { - // Arrange — three logs with canonical rows; close the middle one. Canonical must lose - // only the closed log's rows, and EventCountByLog must drop the closed log's id. - var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData3)); - - var log1Events = new List - { - new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } - }; - - var log2Events = new List - { - new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, - new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 } - }; - - var log3Events = new List - { - new(Constants.LogNameLog3, LogPathType.Channel) { Id = 300, RecordId = 1 } - }; - - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Events)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, log2Events)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData3.Id, log3Events)); - - // Act - var newState = EventTableReducers.ReduceCloseLog(state, new EventTableAction.CloseLog(logData2.Id)); - - // Assert — canonical loses only logData2's rows; count map drops only logData2's id. - Assert.Equal(2, newState.DisplayedEvents.Count); - Assert.DoesNotContain(newState.DisplayedEvents, e => e.OwningLog == Constants.LogNameLog2); - Assert.False(newState.EventCountByLog.ContainsKey(logData2.Id)); - Assert.Equal(1, newState.EventCountByLog[logData1.Id]); - Assert.Equal(1, newState.EventCountByLog[logData3.Id]); - } - - [Fact] - public void ReduceCloseLog_WhenClosingDownToOneLog_ShouldKeepOnlySoleRemainingLogRows() - { - // Arrange — two logs with canonical rows; close one. The Combined table is removed and - // canonical is filtered down to just the remaining log's rows. Exercises the case-1 - // branch (which uses FilterByOwningLog rather than FilterOutOwningLog). - var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - - var log1Events = new List - { - new(Constants.LogNameLog1, LogPathType.Channel) { Id = 100, RecordId = 1 } - }; - - var log2Events = new List - { - new(Constants.LogNameLog2, LogPathType.Channel) { Id = 200, RecordId = 1 }, - new(Constants.LogNameLog2, LogPathType.Channel) { Id = 201, RecordId = 2 } - }; - - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Events)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, log2Events)); - - // Act - var newState = EventTableReducers.ReduceCloseLog(state, new EventTableAction.CloseLog(logData1.Id)); - - // Assert — only logData2's rows remain; logData1 dropped from count map. - Assert.Equal(2, newState.DisplayedEvents.Count); - Assert.All(newState.DisplayedEvents, e => Assert.Equal(Constants.LogNameLog2, e.OwningLog)); - Assert.False(newState.EventCountByLog.ContainsKey(logData1.Id)); - Assert.Equal(2, newState.EventCountByLog[logData2.Id]); - } - - [Fact] - public void ReduceCloseLog_WhenLastTable_ShouldClearAll() - { - // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - - var action = new EventTableAction.CloseLog(logData.Id); - - // Act - var newState = EventTableReducers.ReduceCloseLog(state, action); - - // Assert - Assert.Empty(newState.EventTables); - Assert.Null(newState.ActiveEventLogId); - } - - [Fact] - public void ReduceCloseLog_WhenThreeTables_ShouldKeepCombinedAndRemainingTables() - { - // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var logData3 = new EventLogData(Constants.LogNameLog3, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData3)); - - var action = new EventTableAction.CloseLog(logData1.Id); - - // Act - var newState = EventTableReducers.ReduceCloseLog(state, action); - - // Assert - Assert.Equal(3, newState.EventTables.Count); - Assert.Single(newState.EventTables, t => t.IsCombined); - Assert.Contains(newState.EventTables, t => t.Id == logData2.Id); - Assert.Contains(newState.EventTables, t => t.Id == logData3.Id); - } - - [Fact] - public void ReduceCloseLog_WhenTwoTables_ShouldRemoveCombinedAndSetRemaining() - { - // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - - var action = new EventTableAction.CloseLog(logData1.Id); - - // Act - var newState = EventTableReducers.ReduceCloseLog(state, action); - - // Assert - Assert.Single(newState.EventTables); - Assert.DoesNotContain(newState.EventTables, t => t.IsCombined); - Assert.Equal(logData2.Id, newState.EventTables.First().Id); - Assert.Equal(logData2.Id, newState.ActiveEventLogId); - } - - [Fact] - public void ReduceLoadColumnsCompleted_ShouldUpdateColumns() - { - // Arrange - var state = new EventTableState(); - - var columns = new Dictionary - { - { ColumnName.Level, true }, - { ColumnName.Source, false } - }; - - var widths = new Dictionary - { - { ColumnName.Level, 120 }, - { ColumnName.Source, 250 } - }; - - var order = ColumnDefaults.Order; - var action = new EventTableAction.LoadColumnsCompleted(columns, widths, order); - - // Act - var newState = EventTableReducers.ReduceLoadColumnsCompleted(state, action); - - // Assert - Assert.Equal(2, newState.Columns.Count); - Assert.True(newState.Columns[ColumnName.Level]); - Assert.False(newState.Columns[ColumnName.Source]); - Assert.Equal(120, newState.ColumnWidths[ColumnName.Level]); - Assert.Equal(250, newState.ColumnWidths[ColumnName.Source]); - Assert.Equal(order, newState.ColumnOrder); - } - - [Fact] - public void ReduceReorderColumn_ShouldInsertAfterTarget() - { - // Arrange - var state = new EventTableState - { - ColumnOrder = [ColumnName.Level, ColumnName.DateAndTime, ColumnName.Source, ColumnName.EventId] - }; - - // Move Level after Source (drag right, insertAfter = true) - var action = new EventTableAction.ReorderColumn(ColumnName.Level, ColumnName.Source, true); - - // Act - var newState = EventTableReducers.ReduceReorderColumn(state, action); - - // Assert - Assert.Equal(ColumnName.DateAndTime, newState.ColumnOrder[0]); - Assert.Equal(ColumnName.Source, newState.ColumnOrder[1]); - Assert.Equal(ColumnName.Level, newState.ColumnOrder[2]); - Assert.Equal(ColumnName.EventId, newState.ColumnOrder[3]); - } - - [Fact] - public void ReduceReorderColumn_ShouldInsertBeforeTarget() - { - // Arrange - var state = new EventTableState - { - ColumnOrder = [ColumnName.Level, ColumnName.DateAndTime, ColumnName.Source, ColumnName.EventId] - }; - - // Move Source before Level (drag left, insertAfter = false) - var action = new EventTableAction.ReorderColumn(ColumnName.Source, ColumnName.Level, false); - - // Act - var newState = EventTableReducers.ReduceReorderColumn(state, action); - - // Assert - Assert.Equal(ColumnName.Source, newState.ColumnOrder[0]); - Assert.Equal(ColumnName.Level, newState.ColumnOrder[1]); - Assert.Equal(ColumnName.DateAndTime, newState.ColumnOrder[2]); - Assert.Equal(ColumnName.EventId, newState.ColumnOrder[3]); - } - - [Fact] - public void ReduceReorderColumn_WhenColumnNotInOrder_ShouldReturnUnchanged() - { - // Arrange - var state = new EventTableState - { - ColumnOrder = [ColumnName.Level, ColumnName.DateAndTime] - }; - - var action = new EventTableAction.ReorderColumn(ColumnName.Source, ColumnName.Level, true); - - // Act - var newState = EventTableReducers.ReduceReorderColumn(state, action); - - // Assert - Assert.Equal(state.ColumnOrder, newState.ColumnOrder); - } - - [Fact] - public void ReduceReorderColumn_WhenTargetMissing_ShouldReturnUnchanged() - { - // Arrange - var state = new EventTableState - { - ColumnOrder = [ColumnName.Level, ColumnName.DateAndTime, ColumnName.Source] - }; - - var action = new EventTableAction.ReorderColumn(ColumnName.Level, ColumnName.EventId, true); - - // Act - var newState = EventTableReducers.ReduceReorderColumn(state, action); - - // Assert - Assert.Equal(state.ColumnOrder, newState.ColumnOrder); - } - - [Fact] - public void ReduceReorderColumn_WithDisabledColumnsInterleaved_ShouldInsertRelativeToTarget() - { - // Arrange — full order has disabled columns interleaved among visible ones - var state = new EventTableState - { - ColumnOrder = [ColumnName.Level, ColumnName.ActivityId, ColumnName.DateAndTime, ColumnName.Log, ColumnName.Source] - }; - - // Drag Level right and drop "after Source" (insertAfter = true) - var action = new EventTableAction.ReorderColumn(ColumnName.Level, ColumnName.Source, true); - - // Act - var newState = EventTableReducers.ReduceReorderColumn(state, action); - - // Assert — Level should land immediately after Source, regardless of disabled columns - var levelIndex = newState.ColumnOrder.IndexOf(ColumnName.Level); - var sourceIndex = newState.ColumnOrder.IndexOf(ColumnName.Source); - Assert.Equal(sourceIndex + 1, levelIndex); - } - - [Fact] - public void ReduceSetActiveTable_WhenTableIsLoading_ShouldChangeActive() - { - // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - - var action = new EventTableAction.SetActiveTable(logData.Id); - - // Act - var newState = EventTableReducers.ReduceSetActiveTable(state, action); - - // Assert - Assert.Equal(logData.Id, newState.ActiveEventLogId); - } - - [Fact] - public void ReduceSetActiveTable_WhenTableIsNotLoading_ShouldChangeActive() - { - // Arrange - var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - - // Mark as not loading - state = EventTableReducers.ReduceToggleLoading(state, new EventTableAction.ToggleLoading(logData2.Id)); - - var action = new EventTableAction.SetActiveTable(logData2.Id); - - // Act - var newState = EventTableReducers.ReduceSetActiveTable(state, action); - - // Assert - Assert.Equal(logData2.Id, newState.ActiveEventLogId); - } - - [Fact] - public void ReduceSetActiveTable_WhenTableNotFound_ShouldReturnStateUnchanged() - { - // Arrange - var state = new EventTableState(); - var staleLogId = EventLogId.Create(); - var action = new EventTableAction.SetActiveTable(staleLogId); - - // Act - var newState = EventTableReducers.ReduceSetActiveTable(state, action); - - // Assert - Assert.Same(state, newState); - } - - [Fact] - public void ReduceSetColumnWidth_ShouldUpdateWidth() - { - // Arrange - var state = new EventTableState - { - ColumnWidths = new Dictionary - { - { ColumnName.Level, 100 }, - { ColumnName.Source, 250 } - }.ToImmutableDictionary() - }; - - var action = new EventTableAction.SetColumnWidth(ColumnName.Level, 150); - - // Act - var newState = EventTableReducers.ReduceSetColumnWidth(state, action); - - // Assert - Assert.Equal(150, newState.ColumnWidths[ColumnName.Level]); - Assert.Equal(250, newState.ColumnWidths[ColumnName.Source]); - } - - [Fact] - public void ReduceSetColumnWidth_WhenColumnNotYetInWidths_ShouldAddIt() - { - // Arrange - var state = new EventTableState(); - - var action = new EventTableAction.SetColumnWidth(ColumnName.EventId, 90); - - // Act - var newState = EventTableReducers.ReduceSetColumnWidth(state, action); - - // Assert - Assert.Equal(90, newState.ColumnWidths[ColumnName.EventId]); - } - - [Fact] - public void ReduceSetOrderBy_WhenToggledOff_ShouldSortPerLogListsByEffectiveComparator() - { - // After the user clicks the active column header to deselect it (OrderBy reset to null), - // per-log lists must remain sorted by the same comparator the combined merge will use - // (DateAndTime). Otherwise a subsequent ReduceUpdateCombinedEvents merges RecordId-sorted - // inputs under a DateAndTime comparator and produces silently incorrect output. - var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var state = new EventTableState { OrderBy = ColumnName.Level, IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - - var events = new List - { - new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, - new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } - }; - - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, events)); - - // Act — user deselects the active column - var toggledState = EventTableReducers.ReduceSetOrderBy(state, new EventTableAction.SetOrderBy(ColumnName.Level)); - - // Assert — toggle-off forces descending; the order distinguishes a DateAndTime sort - // (events ordered +40s then +20s) from the buggy RecordId sort (which would order - // RecordId=2 (+20s) before RecordId=1 (+40s)). - Assert.Null(toggledState.OrderBy); - Assert.True(toggledState.IsDescending); - var sortedEvents = toggledState.DisplayedEvents; - Assert.Equal(baseTime.AddSeconds(40), sortedEvents[0].TimeCreated); - Assert.Equal(baseTime.AddSeconds(20), sortedEvents[1].TimeCreated); - } - - [Fact] - public void ReduceSetOrderBy_WithNewColumn_ShouldUpdateOrderBy() - { - // Arrange - var state = new EventTableState { OrderBy = ColumnName.DateAndTime }; - var action = new EventTableAction.SetOrderBy(ColumnName.Level); - - // Act - var newState = EventTableReducers.ReduceSetOrderBy(state, action); - - // Assert - Assert.Equal(ColumnName.Level, newState.OrderBy); - } - - [Fact] - public void ReduceSetOrderBy_WithSameColumn_ShouldToggleSorting() - { - // Arrange - var state = new EventTableState { OrderBy = ColumnName.Level, IsDescending = true }; - var action = new EventTableAction.SetOrderBy(ColumnName.Level); - - // Act - var newState = EventTableReducers.ReduceSetOrderBy(state, action); - - // Assert - Assert.Null(newState.OrderBy); - Assert.True(newState.IsDescending); - } - - [Fact] - public void ReduceToggleLoading_ShouldToggleLoadingState() - { - // Arrange - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - - Assert.True(state.EventTables.First().IsLoading); - - var action = new EventTableAction.ToggleLoading(logData.Id); - - // Act - var newState = EventTableReducers.ReduceToggleLoading(state, action); - - // Assert - Assert.False(newState.EventTables.First(t => t.Id == logData.Id).IsLoading); - } - - [Fact] - public void ReduceToggleLoading_WhenTableNotFound_ShouldReturnSameState() - { - // Arrange - var state = new EventTableState(); - var action = new EventTableAction.ToggleLoading(EventLogId.Create()); - - // Act - var newState = EventTableReducers.ReduceToggleLoading(state, action); - - // Assert - Assert.Same(state, newState); - } - - [Fact] - public void ReduceToggleSorting_ShouldToggleIsDescending() - { - // Arrange - var state = new EventTableState { IsDescending = true }; - - // Act - var newState = EventTableReducers.ReduceToggleSorting(state); - - // Assert - Assert.False(newState.IsDescending); - } - - [Fact] - public void ReduceToggleSorting_WhenOrderByIsNull_ShouldPreserveNullOrderBy() - { - // ToggleSorting only flips IsDescending. When the user has previously deselected the - // active column (OrderBy is null), toggling sort direction must preserve that null so - // the UI does not silently re-light a column header. Sort still uses the effective - // (DateAndTime) comparator inside SortDisplayEvents. - var baseTime = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Utc); - var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); - var state = new EventTableState { OrderBy = null, IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); - - var events = new List - { - new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 10, RecordId = 1, TimeCreated = baseTime.AddSeconds(40) }, - new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2, TimeCreated = baseTime.AddSeconds(20) } - }; - - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, events)); - - // Act - var toggledState = EventTableReducers.ReduceToggleSorting(state); - - // Assert - Assert.Null(toggledState.OrderBy); - Assert.True(toggledState.IsDescending); - var sortedEvents = toggledState.DisplayedEvents; - Assert.Equal(baseTime.AddSeconds(40), sortedEvents[0].TimeCreated); - Assert.Equal(baseTime.AddSeconds(20), sortedEvents[1].TimeCreated); - } - - [Fact] - public void ReduceUpdateDisplayedEvents_ShouldUpdateSlicesForLogsInActiveLogs() - { - // Arrange — two logs added, then a filter-driven UpdateDisplayedEvents arrives with - // post-filter results for both logs. - var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); - - var log1Events = new List - { - new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 } - }; - - var log2Events = new List - { - new(Constants.LogNameLog2, LogPathType.Channel) { Id = 20, RecordId = 2 } - }; - - var activeLogs = new Dictionary> - { - { logData1.Id, log1Events }, - { logData2.Id, log2Events } - }; - - var action = new EventTableAction.UpdateDisplayedEvents(activeLogs); - - // Act - var newState = EventTableReducers.ReduceUpdateDisplayedEvents(state, action); - - // Assert — canonical contains both logs' filtered events; per-log counts updated. - Assert.Equal(2, newState.DisplayedEvents.Count); - Assert.Equal(1, newState.EventCountByLog[logData1.Id]); - Assert.Equal(1, newState.EventCountByLog[logData2.Id]); - } - - [Fact] - public void ReduceUpdateDisplayedEvents_WhenActiveLogsContainsClosedLogId_ShouldSkipIt() - { - // Arrange — filter result includes a log id whose tab was closed mid-flight - var openLog = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); - var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(openLog)); - - var staleLogId = EventLogId.Create(); - var openLogEvents = new List - { - new(Constants.LogNameLog1, LogPathType.Channel) { Id = 10, RecordId = 1 } - }; - - var staleEvents = new List - { - new("ClosedLog", LogPathType.Channel) { Id = 99, RecordId = 99 } - }; - - var activeLogs = new Dictionary> - { - { openLog.Id, openLogEvents }, - { staleLogId, staleEvents } - }; - - // Act - var newState = EventTableReducers.ReduceUpdateDisplayedEvents( - state, - new EventTableAction.UpdateDisplayedEvents(activeLogs)); - - // Assert — closed log id contributes nothing to canonical or to the count map - Assert.Single(newState.DisplayedEvents); - Assert.Equal(1L, newState.DisplayedEvents[0].RecordId); - Assert.False(newState.EventCountByLog.ContainsKey(staleLogId)); - } - [Fact] public void ReduceUpdateDisplayedEvents_WhenLogIsNotInActiveLogs_ShouldPreserveExistingCanonicalRows() { @@ -1213,7 +613,7 @@ public void ReduceUpdateDisplayedEvents_WhenLogIsNotInActiveLogs_ShouldPreserveE // load could be silently scrubbed by a stale filter result. var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var loadedEvents = new List { @@ -1221,13 +621,13 @@ public void ReduceUpdateDisplayedEvents_WhenLogIsNotInActiveLogs_ShouldPreserveE new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } }; - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, loadedEvents)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData.Id, loadedEvents)); var emptyActiveLogs = new Dictionary>(); - var action = new EventTableAction.UpdateDisplayedEvents(emptyActiveLogs); + var action = new UpdateDisplayedEventsAction(emptyActiveLogs); // Act - var newState = EventTableReducers.ReduceUpdateDisplayedEvents(state, action); + var newState = Reducers.ReduceUpdateDisplayedEvents(state, action); // Assert — table still exists, canonical rows preserved, count map preserved. Assert.Single(newState.EventTables); @@ -1244,8 +644,8 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); var log1Loaded = new List { @@ -1260,8 +660,8 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude new(Constants.LogNameLog2, LogPathType.Channel) { Id = 202, RecordId = 3 } }; - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Loaded)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, log2Loaded)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData1.Id, log1Loaded)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData2.Id, log2Loaded)); var log1Filtered = new List { @@ -1274,9 +674,9 @@ public void ReduceUpdateDisplayedEvents_WhenSomeLogsOmitted_ShouldReplaceInclude }; // Act - var newState = EventTableReducers.ReduceUpdateDisplayedEvents( + var newState = Reducers.ReduceUpdateDisplayedEvents( state, - new EventTableAction.UpdateDisplayedEvents(activeLogs)); + new UpdateDisplayedEventsAction(activeLogs)); // Assert — log A reduced from 2 to 1, log B's 3 rows untouched; counts reflect both. Assert.Equal(4, newState.DisplayedEvents.Count); @@ -1292,7 +692,7 @@ public void ReduceUpdateDisplayedEvents_WhenTableComputerNameEmpty_ShouldLatchFr // Arrange — log first becomes visible via UpdateDisplayedEvents (filter clear), not via append var logData = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var revealedEvents = new List { @@ -1306,9 +706,9 @@ public void ReduceUpdateDisplayedEvents_WhenTableComputerNameEmpty_ShouldLatchFr }; // Act - var newState = EventTableReducers.ReduceUpdateDisplayedEvents( + var newState = Reducers.ReduceUpdateDisplayedEvents( state, - new EventTableAction.UpdateDisplayedEvents(activeLogs)); + new UpdateDisplayedEventsAction(activeLogs)); // Assert — UpdateDisplayedEvents also latches ComputerName, not just the append paths var updatedTable = newState.EventTables.First(t => t.Id == logData.Id); @@ -1321,7 +721,7 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti // Arrange — partial AppendTableEvents land first (live-load deltas), then UpdateTable arrives with the full filtered list var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var partial1 = new List { @@ -1329,18 +729,18 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } }; - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, partial1)); + new AppendTableEventsAction(logData.Id, partial1)); var partial2 = new List { new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 12, RecordId = 3 } }; - state = EventTableReducers.ReduceAppendTableEvents( + state = Reducers.ReduceAppendTableEvents( state, - new EventTableAction.AppendTableEvents(logData.Id, partial2)); + new AppendTableEventsAction(logData.Id, partial2)); Assert.Equal(3, state.DisplayedEvents.Count); @@ -1353,9 +753,9 @@ public void ReduceUpdateTable_AfterPartialAppends_ShouldReplaceNotMergeWithParti }; // Act - state = EventTableReducers.ReduceUpdateTable( + state = Reducers.ReduceUpdateTable( state, - new EventTableAction.UpdateTable(logData.Id, fullLoad)); + new UpdateTableAction(logData.Id, fullLoad)); // Assert — partials are dropped before merging the full slice; canonical is exactly the full load Assert.Equal(4, state.DisplayedEvents.Count); @@ -1369,7 +769,7 @@ public void ReduceUpdateTable_ShouldUpdateTableEvents() // Arrange var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState(); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var events = new List { @@ -1377,10 +777,10 @@ public void ReduceUpdateTable_ShouldUpdateTableEvents() EventUtils.CreateTestEvent(200) }; - var action = new EventTableAction.UpdateTable(logData.Id, events); + var action = new UpdateTableAction(logData.Id, events); // Act - var newState = EventTableReducers.ReduceUpdateTable(state, action); + var newState = Reducers.ReduceUpdateTable(state, action); // Assert var updatedTable = newState.EventTables.First(t => t.Id == logData.Id); @@ -1395,7 +795,7 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat // Arrange — first UpdateTable populates canonical with two events var logData = new EventLogData(Constants.LogNameTestLog, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData)); var firstLoad = new List { @@ -1403,7 +803,7 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat new(Constants.LogNameTestLog, LogPathType.Channel) { Id = 11, RecordId = 2 } }; - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, firstLoad)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData.Id, firstLoad)); Assert.Equal(2, state.DisplayedEvents.Count); var secondLoad = new List @@ -1414,7 +814,7 @@ public void ReduceUpdateTable_WhenCalledTwiceForSameLog_ShouldReplaceNotDuplicat }; // Act — second UpdateTable for the same log (e.g., filter-driven reload) - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData.Id, secondLoad)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData.Id, secondLoad)); // Assert — canonical reflects only the second load; first load is replaced, not appended Assert.Equal(3, state.DisplayedEvents.Count); @@ -1429,8 +829,8 @@ public void ReduceUpdateTable_WhenDescendingOrderRequested_ShouldMergeInDescendi var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = true }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); var eventsLog1 = new List { @@ -1445,8 +845,8 @@ public void ReduceUpdateTable_WhenDescendingOrderRequested_ShouldMergeInDescendi }; // Act - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, eventsLog1)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, eventsLog2)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData1.Id, eventsLog1)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData2.Id, eventsLog2)); // Assert — canonical view in descending RecordId order Assert.Equal(4, state.DisplayedEvents.Count); @@ -1463,8 +863,8 @@ public void ReduceUpdateTable_WhenSecondLogIsEmpty_ShouldKeepFirstLogEvents() var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); var eventsLog1 = new List { @@ -1473,8 +873,8 @@ public void ReduceUpdateTable_WhenSecondLogIsEmpty_ShouldKeepFirstLogEvents() }; // Act - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, eventsLog1)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, [])); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData1.Id, eventsLog1)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData2.Id, [])); // Assert — canonical view contains only the populated log's events Assert.Equal(2, state.DisplayedEvents.Count); @@ -1489,8 +889,8 @@ public void ReduceUpdateTable_WhenSecondLogPopulated_ShouldMergeIntoCanonicalInS var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); var eventsLog1 = new List { @@ -1505,8 +905,8 @@ public void ReduceUpdateTable_WhenSecondLogPopulated_ShouldMergeIntoCanonicalInS }; // Act — UpdateTable maintains the canonical view atomically; no follow-up call needed. - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, eventsLog1)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, eventsLog2)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData1.Id, eventsLog1)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData2.Id, eventsLog2)); // Assert — canonical view interleaves both logs in RecordId order Assert.Equal(4, state.DisplayedEvents.Count); @@ -1525,10 +925,10 @@ public void ReduceUpdateTable_WhenTableNotFound_ShouldReturnStateUnchanged() var state = new EventTableState(); var staleLogId = EventLogId.Create(); var events = new List { EventUtils.CreateTestEvent(100) }; - var action = new EventTableAction.UpdateTable(staleLogId, events); + var action = new UpdateTableAction(staleLogId, events); // Act — stale UpdateTable for a non-existent table - var newState = EventTableReducers.ReduceUpdateTable(state, action); + var newState = Reducers.ReduceUpdateTable(state, action); // Assert — state unchanged, no exception thrown Assert.Same(state, newState); @@ -1555,12 +955,12 @@ public void ReduceUpdateTable_WhenTimeCreatedDivergesFromRecordId_ShouldMergeByT var logData1 = new EventLogData(Constants.LogNameLog1, LogPathType.Channel, []); var logData2 = new EventLogData(Constants.LogNameLog2, LogPathType.Channel, []); var state = new EventTableState { IsDescending = false }; - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData1)); - state = EventTableReducers.ReduceAddTable(state, new EventTableAction.AddTable(logData2)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData1)); + state = Reducers.ReduceAddTable(state, new AddTableAction(logData2)); // Act - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData1.Id, log1Events)); - state = EventTableReducers.ReduceUpdateTable(state, new EventTableAction.UpdateTable(logData2.Id, log2Events)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData1.Id, log1Events)); + state = Reducers.ReduceUpdateTable(state, new UpdateTableAction(logData2.Id, log2Events)); // Assert — canonical comes out time-ordered, not RecordId-ordered var actualTimes = state.DisplayedEvents.Select(e => e.TimeCreated).ToList(); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs similarity index 79% rename from tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheEffectsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs index ba1ba96c..e81a4a92 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs @@ -10,7 +10,7 @@ namespace EventLogExpert.UI.Tests.Store.FilterCache; -public sealed class FilterCacheEffectsTests +public sealed class EffectsTests { [Fact] public async Task HandleAddFavoriteFilter_ShouldPersistToPreferences() @@ -21,7 +21,7 @@ public async Task HandleAddFavoriteFilter_ShouldPersistToPreferences() var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( existingFavorites); - var action = new FilterCacheAction.AddFavoriteFilter(Constants.FilterLevelEqualsError); + var action = new AddFavoriteFilterAction(Constants.FilterLevelEqualsError); // Act await effects.HandleAddFavoriteFilter(action, mockDispatcher); @@ -42,13 +42,13 @@ public async Task HandleAddFavoriteFilter_WhenFilterAlreadyExists_ShouldNotDispa var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( existingFavorites); - var action = new FilterCacheAction.AddFavoriteFilter(Constants.FilterIdEquals100); + var action = new AddFavoriteFilterAction(Constants.FilterIdEquals100); // Act await effects.HandleAddFavoriteFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -60,13 +60,13 @@ public async Task HandleAddFavoriteFilter_WhenFilterDoesNotExist_ShouldAddToFavo var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( existingFavorites); - var action = new FilterCacheAction.AddFavoriteFilter(Constants.FilterIdEquals200); + var action = new AddFavoriteFilterAction(Constants.FilterIdEquals200); // Act await effects.HandleAddFavoriteFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Filters.Count == 2 && x.Filters.Contains(Constants.FilterIdEquals100) && x.Filters.Contains(Constants.FilterIdEquals200))); @@ -81,7 +81,7 @@ public async Task HandleAddRecentFilter_ShouldPersistToPreferences() var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( recentFilters: existingRecent); - var action = new FilterCacheAction.AddRecentFilter(Constants.FilterLevelEqualsError); + var action = new AddRecentFilterAction(Constants.FilterLevelEqualsError); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); @@ -102,13 +102,13 @@ public async Task HandleAddRecentFilter_WhenFilterAlreadyExists_ShouldNotDispatc var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( recentFilters: existingRecent); - var action = new FilterCacheAction.AddRecentFilter(Constants.FilterIdEquals100); + var action = new AddRecentFilterAction(Constants.FilterIdEquals100); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -120,13 +120,13 @@ public async Task HandleAddRecentFilter_WhenFilterExistsCaseInsensitive_ShouldNo var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( recentFilters: existingRecent); - var action = new FilterCacheAction.AddRecentFilter("ID == 100"); + var action = new AddRecentFilterAction("ID == 100"); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -135,13 +135,13 @@ public async Task HandleAddRecentFilter_WhenFilterIsEmpty_ShouldNotDispatch() // Arrange var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(); - var action = new FilterCacheAction.AddRecentFilter(""); + var action = new AddRecentFilterAction(""); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -153,13 +153,13 @@ public async Task HandleAddRecentFilter_WhenFilterIsNew_ShouldAddToRecent() var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( recentFilters: existingRecent); - var action = new FilterCacheAction.AddRecentFilter(Constants.FilterLevelEqualsError); + var action = new AddRecentFilterAction(Constants.FilterLevelEqualsError); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Filters.Count() == 2)); } @@ -169,13 +169,13 @@ public async Task HandleAddRecentFilter_WhenFilterIsWhitespace_ShouldNotDispatch // Arrange var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects(); - var action = new FilterCacheAction.AddRecentFilter(" "); + var action = new AddRecentFilterAction(" "); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -191,13 +191,13 @@ public async Task HandleAddRecentFilter_WhenMaxReached_ShouldDequeueOldest() var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( recentFilters: existingRecent); - var action = new FilterCacheAction.AddRecentFilter("NewFilter"); + var action = new AddRecentFilterAction("NewFilter"); // Act await effects.HandleAddRecentFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.Filters.Count() == 20 && !a.Filters.Contains("Filter1") && a.Filters.Contains("NewFilter"))); @@ -218,13 +218,13 @@ public async Task HandleImportFavorites_ShouldBeCaseInsensitive() Constants.FilterLevelEqualsError }; - var action = new FilterCacheAction.ImportFavorites(filtersToImport); + var action = new ImportFavoritesAction(filtersToImport); // Act await effects.HandleImportFavorites(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.Filters.Count == 2 && a.Filters.Contains(Constants.FilterLevelEqualsError))); } @@ -239,7 +239,7 @@ public async Task HandleImportFavorites_ShouldPersistToPreferences() existingFavorites); var filtersToImport = new List { Constants.FilterLevelEqualsError }; - var action = new FilterCacheAction.ImportFavorites(filtersToImport); + var action = new ImportFavoritesAction(filtersToImport); // Act await effects.HandleImportFavorites(action, mockDispatcher); @@ -269,13 +269,13 @@ public async Task HandleImportFavorites_WhenFiltersAlreadyExist_ShouldNotAddDupl Constants.FilterSourceContainsTest }; - var action = new FilterCacheAction.ImportFavorites(filtersToImport); + var action = new ImportFavoritesAction(filtersToImport); // Act await effects.HandleImportFavorites(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Filters.Count == 3 && x.Filters.Contains(Constants.FilterSourceContainsTest))); } @@ -295,13 +295,13 @@ public async Task HandleImportFavorites_WhenFiltersAreNew_ShouldAddAll() Constants.FilterSourceContainsTest }; - var action = new FilterCacheAction.ImportFavorites(filtersToImport); + var action = new ImportFavoritesAction(filtersToImport); // Act await effects.HandleImportFavorites(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Filters.Count == 3 && x.Filters.Contains(Constants.FilterIdEquals100) && x.Filters.Contains(Constants.FilterLevelEqualsError) && @@ -324,13 +324,13 @@ public async Task HandleImportFavorites_WhenImportContainsCaseInsensitiveDuplica Constants.FilterLevelEqualsError }; - var action = new FilterCacheAction.ImportFavorites(filtersToImport); + var action = new ImportFavoritesAction(filtersToImport); // Act await effects.HandleImportFavorites(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(a => + mockDispatcher.Received(1).Dispatch(Arg.Is(a => a.Filters.Count == 2 && a.Filters.Contains("Id == 100") && a.Filters.Contains(Constants.FilterLevelEqualsError))); @@ -358,15 +358,15 @@ public async Task HandleLoadFilters_ShouldLoadBothFavoritesAndRecent() var mockState = Substitute.For>(); mockState.Value.Returns(new FilterCacheState()); - var effects = new FilterCacheEffects(mockPreferencesProvider, mockState); + var effects = new Effects(mockPreferencesProvider, mockState); var mockDispatcher = Substitute.For(); - var action = new FilterCacheAction.LoadFilters(); + var action = new LoadFiltersAction(); // Act await effects.HandleLoadFilters(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FavoriteFilters.Count == 2 && x.RecentFilters.Count() == 1 && x.FavoriteFilters.Contains(Constants.FilterIdEquals100) && @@ -385,15 +385,15 @@ public async Task HandleLoadFilters_WhenPreferencesEmpty_ShouldLoadEmptyLists() var mockState = Substitute.For>(); mockState.Value.Returns(new FilterCacheState()); - var effects = new FilterCacheEffects(mockPreferencesProvider, mockState); + var effects = new Effects(mockPreferencesProvider, mockState); var mockDispatcher = Substitute.For(); - var action = new FilterCacheAction.LoadFilters(); + var action = new LoadFiltersAction(); // Act await effects.HandleLoadFilters(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FavoriteFilters.Count == 0 && !x.RecentFilters.Any())); } @@ -412,7 +412,7 @@ public async Task HandleRemoveFavoriteFilter_ShouldPersistBothPreferences() existingFavorites, existingRecent); - var action = new FilterCacheAction.RemoveFavoriteFilter(Constants.FilterIdEquals100); + var action = new RemoveFavoriteFilterAction(Constants.FilterIdEquals100); // Act await effects.HandleRemoveFavoriteFilter(action, mockDispatcher); @@ -440,13 +440,13 @@ public async Task HandleRemoveFavoriteFilter_WhenFilterInRecent_ShouldRemoveFrom existingFavorites, existingRecent); - var action = new FilterCacheAction.RemoveFavoriteFilter(Constants.FilterIdEquals100); + var action = new RemoveFavoriteFilterAction(Constants.FilterIdEquals100); // Act await effects.HandleRemoveFavoriteFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FavoriteFilters.Count == 1 && !x.FavoriteFilters.Contains(Constants.FilterIdEquals100) && x.RecentFilters.Count() == 1 && @@ -462,13 +462,13 @@ public async Task HandleRemoveFavoriteFilter_WhenFilterNotInFavorites_ShouldNotD var (effects, mockDispatcher, mockPreferencesProvider) = CreateEffects( existingFavorites); - var action = new FilterCacheAction.RemoveFavoriteFilter(Constants.FilterLevelEqualsError); + var action = new RemoveFavoriteFilterAction(Constants.FilterLevelEqualsError); // Act await effects.HandleRemoveFavoriteFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -485,13 +485,13 @@ public async Task HandleRemoveFavoriteFilter_WhenFilterNotInRecent_ShouldAddToRe existingFavorites, existingRecent); - var action = new FilterCacheAction.RemoveFavoriteFilter(Constants.FilterIdEquals100); + var action = new RemoveFavoriteFilterAction(Constants.FilterIdEquals100); // Act await effects.HandleRemoveFavoriteFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FavoriteFilters.Count == 1 && !x.FavoriteFilters.Contains(Constants.FilterIdEquals100) && x.RecentFilters.Count() == 2 && @@ -514,20 +514,20 @@ public async Task HandleRemoveFavoriteFilter_WhenRecentIsFull_ShouldDequeueOldes existingFavorites, existingRecent); - var action = new FilterCacheAction.RemoveFavoriteFilter(Constants.FilterIdEquals100); + var action = new RemoveFavoriteFilterAction(Constants.FilterIdEquals100); // Act await effects.HandleRemoveFavoriteFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FavoriteFilters.Count == 0 && x.RecentFilters.Count() == 20 && !x.RecentFilters.Contains("Filter1") && x.RecentFilters.Contains(Constants.FilterIdEquals100))); } - private static (FilterCacheEffects effects, IDispatcher mockDispatcher, IPreferencesProvider mockPreferencesProvider) CreateEffects( + private static (Effects effects, IDispatcher mockDispatcher, IPreferencesProvider mockPreferencesProvider) CreateEffects( ImmutableList? favoriteFilters = null, ImmutableQueue? recentFilters = null) { @@ -541,7 +541,7 @@ private static (FilterCacheEffects effects, IDispatcher mockDispatcher, IPrefere var mockPreferencesProvider = Substitute.For(); - var effects = new FilterCacheEffects(mockPreferencesProvider, mockState); + var effects = new Effects(mockPreferencesProvider, mockState); var mockDispatcher = Substitute.For(); return (effects, mockDispatcher, mockPreferencesProvider); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheStoreTests.cs index 4cb89d62..9a638377 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/FilterCacheStoreTests.cs @@ -9,19 +9,6 @@ namespace EventLogExpert.UI.Tests.Store.FilterCache; public sealed class FilterCacheStoreTests { - [Fact] - public void FilterCacheAction_AddFavoriteFilter_ShouldStoreFilter() - { - // Arrange - var filter = Constants.FilterIdEquals100; - - // Act - var action = new FilterCacheAction.AddFavoriteFilter(filter); - - // Assert - Assert.Equal(filter, action.Filter); - } - [Fact] public void FilterCacheAction_AddFavoriteFilterCompleted_ShouldStoreFilters() { @@ -29,7 +16,7 @@ public void FilterCacheAction_AddFavoriteFilterCompleted_ShouldStoreFilters() var filters = ImmutableList.Create(Constants.FilterIdEquals100, Constants.FilterIdEquals200); // Act - var action = new FilterCacheAction.AddFavoriteFilterCompleted(filters); + var action = new AddFavoriteFilterCompletedAction(filters); // Assert Assert.Equal(2, action.Filters.Count); @@ -38,13 +25,13 @@ public void FilterCacheAction_AddFavoriteFilterCompleted_ShouldStoreFilters() } [Fact] - public void FilterCacheAction_AddRecentFilter_ShouldStoreFilter() + public void FilterCacheAction_AddFavoriteFilter_ShouldStoreFilter() { // Arrange - var filter = Constants.FilterLevelEqualsError; + var filter = Constants.FilterIdEquals100; // Act - var action = new FilterCacheAction.AddRecentFilter(filter); + var action = new AddFavoriteFilterAction(filter); // Assert Assert.Equal(filter, action.Filter); @@ -57,12 +44,25 @@ public void FilterCacheAction_AddRecentFilterCompleted_ShouldStoreFilters() var filters = ImmutableQueue.Create(Constants.FilterIdEquals100, Constants.FilterIdEquals200); // Act - var action = new FilterCacheAction.AddRecentFilterCompleted(filters); + var action = new AddRecentFilterCompletedAction(filters); // Assert Assert.Equal(2, action.Filters.Count()); } + [Fact] + public void FilterCacheAction_AddRecentFilter_ShouldStoreFilter() + { + // Arrange + var filter = Constants.FilterLevelEqualsError; + + // Act + var action = new AddRecentFilterAction(filter); + + // Assert + Assert.Equal(filter, action.Filter); + } + [Fact] public void FilterCacheAction_ImportFavorites_ShouldStoreFilters() { @@ -70,7 +70,7 @@ public void FilterCacheAction_ImportFavorites_ShouldStoreFilters() var filters = new List { Constants.FilterIdEquals100, Constants.FilterIdEquals200 }; // Act - var action = new FilterCacheAction.ImportFavorites(filters); + var action = new ImportFavoritesAction(filters); // Assert Assert.Equal(2, action.Filters.Count); @@ -84,7 +84,7 @@ public void FilterCacheAction_LoadFiltersCompleted_ShouldStoreBothFilters() var recent = ImmutableQueue.Create(Constants.FilterIdEquals200); // Act - var action = new FilterCacheAction.LoadFiltersCompleted(favorites, recent); + var action = new LoadFiltersCompletedAction(favorites, recent); // Assert Assert.Single(action.FavoriteFilters); @@ -92,31 +92,31 @@ public void FilterCacheAction_LoadFiltersCompleted_ShouldStoreBothFilters() } [Fact] - public void FilterCacheAction_RemoveFavoriteFilter_ShouldStoreFilter() + public void FilterCacheAction_RemoveFavoriteFilterCompleted_ShouldStoreBothFilters() { // Arrange - var filter = Constants.FilterIdEquals100; + var favorites = ImmutableList.Create(Constants.FilterIdEquals100); + var recent = ImmutableQueue.Create(Constants.FilterIdEquals200); // Act - var action = new FilterCacheAction.RemoveFavoriteFilter(filter); + var action = new RemoveFavoriteFilterCompletedAction(favorites, recent); // Assert - Assert.Equal(filter, action.Filter); + Assert.Single(action.FavoriteFilters); + Assert.Single(action.RecentFilters); } [Fact] - public void FilterCacheAction_RemoveFavoriteFilterCompleted_ShouldStoreBothFilters() + public void FilterCacheAction_RemoveFavoriteFilter_ShouldStoreFilter() { // Arrange - var favorites = ImmutableList.Create(Constants.FilterIdEquals100); - var recent = ImmutableQueue.Create(Constants.FilterIdEquals200); + var filter = Constants.FilterIdEquals100; // Act - var action = new FilterCacheAction.RemoveFavoriteFilterCompleted(favorites, recent); + var action = new RemoveFavoriteFilterAction(filter); // Assert - Assert.Single(action.FavoriteFilters); - Assert.Single(action.RecentFilters); + Assert.Equal(filter, action.Filter); } [Fact] @@ -139,9 +139,9 @@ public void IntegrationTest_AddMultipleFavorites() // Act - Add first favorite var favorites1 = ImmutableList.Create(Constants.FilterIdEquals100); - state = FilterCacheReducers.ReduceAddFavoriteFilterCompleted( + state = Reducers.ReduceAddFavoriteFilterCompleted( state, - new FilterCacheAction.AddFavoriteFilterCompleted(favorites1)); + new AddFavoriteFilterCompletedAction(favorites1)); // Assert Assert.Single(state.FavoriteFilters); @@ -149,9 +149,9 @@ public void IntegrationTest_AddMultipleFavorites() // Act - Add second favorite var favorites2 = favorites1.Add(Constants.FilterIdEquals200); - state = FilterCacheReducers.ReduceAddFavoriteFilterCompleted( + state = Reducers.ReduceAddFavoriteFilterCompleted( state, - new FilterCacheAction.AddFavoriteFilterCompleted(favorites2)); + new AddFavoriteFilterCompletedAction(favorites2)); // Assert Assert.Equal(2, state.FavoriteFilters.Count); @@ -168,9 +168,9 @@ public void IntegrationTest_AddMultipleRecent() // Act - Add first recent var recent1 = ImmutableQueue.Create(Constants.FilterIdEquals100); - state = FilterCacheReducers.ReduceAddRecentFilterCompleted( + state = Reducers.ReduceAddRecentFilterCompleted( state, - new FilterCacheAction.AddRecentFilterCompleted(recent1)); + new AddRecentFilterCompletedAction(recent1)); // Assert Assert.Single(state.RecentFilters); @@ -178,9 +178,9 @@ public void IntegrationTest_AddMultipleRecent() // Act - Add second recent var recent2 = recent1.Enqueue(Constants.FilterIdEquals200); - state = FilterCacheReducers.ReduceAddRecentFilterCompleted( + state = Reducers.ReduceAddRecentFilterCompleted( state, - new FilterCacheAction.AddRecentFilterCompleted(recent2)); + new AddRecentFilterCompletedAction(recent2)); // Assert Assert.Equal(2, state.RecentFilters.Count()); @@ -200,9 +200,9 @@ public void IntegrationTest_ComplexFilterManagement() var initialRecent = ImmutableQueue.Create(Constants.FilterLevelEqualsError); - state = FilterCacheReducers.ReduceLoadFiltersCompleted( + state = Reducers.ReduceLoadFiltersCompleted( state, - new FilterCacheAction.LoadFiltersCompleted(initialFavorites, initialRecent)); + new LoadFiltersCompletedAction(initialFavorites, initialRecent)); // Assert initial state Assert.Equal(2, state.FavoriteFilters.Count); @@ -211,9 +211,9 @@ public void IntegrationTest_ComplexFilterManagement() // Act - Add new favorite var updatedFavorites = state.FavoriteFilters.Add(Constants.FilterIdGreaterThan100); - state = FilterCacheReducers.ReduceAddFavoriteFilterCompleted( + state = Reducers.ReduceAddFavoriteFilterCompleted( state, - new FilterCacheAction.AddFavoriteFilterCompleted(updatedFavorites)); + new AddFavoriteFilterCompletedAction(updatedFavorites)); // Assert Assert.Equal(3, state.FavoriteFilters.Count); @@ -221,9 +221,9 @@ public void IntegrationTest_ComplexFilterManagement() // Act - Add new recent var updatedRecent = state.RecentFilters.Enqueue(Constants.FilterSourceContainsTest); - state = FilterCacheReducers.ReduceAddRecentFilterCompleted( + state = Reducers.ReduceAddRecentFilterCompleted( state, - new FilterCacheAction.AddRecentFilterCompleted(updatedRecent)); + new AddRecentFilterCompletedAction(updatedRecent)); // Assert Assert.Equal(2, state.RecentFilters.Count()); @@ -232,9 +232,9 @@ public void IntegrationTest_ComplexFilterManagement() var finalFavorites = state.FavoriteFilters.Remove(Constants.FilterIdEquals100); var finalRecent = state.RecentFilters.Enqueue(Constants.FilterIdEquals100); - state = FilterCacheReducers.ReduceRemoveFavoriteFilterCompleted( + state = Reducers.ReduceRemoveFavoriteFilterCompleted( state, - new FilterCacheAction.RemoveFavoriteFilterCompleted(finalFavorites, finalRecent)); + new RemoveFavoriteFilterCompletedAction(finalFavorites, finalRecent)); // Assert final state Assert.Equal(2, state.FavoriteFilters.Count); @@ -253,9 +253,9 @@ public void IntegrationTest_LoadThenAddFilters() var initialFavorites = ImmutableList.Create(Constants.FilterIdEquals100); var initialRecent = ImmutableQueue.Create(Constants.FilterLevelEqualsError); - state = FilterCacheReducers.ReduceLoadFiltersCompleted( + state = Reducers.ReduceLoadFiltersCompleted( state, - new FilterCacheAction.LoadFiltersCompleted(initialFavorites, initialRecent)); + new LoadFiltersCompletedAction(initialFavorites, initialRecent)); // Assert Assert.Single(state.FavoriteFilters); @@ -264,9 +264,9 @@ public void IntegrationTest_LoadThenAddFilters() // Act - Add new favorite var updatedFavorites = state.FavoriteFilters.Add(Constants.FilterIdEquals200); - state = FilterCacheReducers.ReduceAddFavoriteFilterCompleted( + state = Reducers.ReduceAddFavoriteFilterCompleted( state, - new FilterCacheAction.AddFavoriteFilterCompleted(updatedFavorites)); + new AddFavoriteFilterCompletedAction(updatedFavorites)); // Assert Assert.Equal(2, state.FavoriteFilters.Count); @@ -290,9 +290,9 @@ public void IntegrationTest_RemoveFavoriteAndAddToRecent() var updatedFavorites = state.FavoriteFilters.Remove(Constants.FilterIdEquals100); var updatedRecent = state.RecentFilters.Enqueue(Constants.FilterIdEquals100); - state = FilterCacheReducers.ReduceRemoveFavoriteFilterCompleted( + state = Reducers.ReduceRemoveFavoriteFilterCompleted( state, - new FilterCacheAction.RemoveFavoriteFilterCompleted(updatedFavorites, updatedRecent)); + new RemoveFavoriteFilterCompletedAction(updatedFavorites, updatedRecent)); // Assert Assert.Single(state.FavoriteFilters); @@ -309,10 +309,10 @@ public void ReduceAddFavoriteFilterCompleted_ShouldNotAffectRecentFilters() var existingRecent = ImmutableQueue.Create(Constants.FilterLevelEqualsError); var state = new FilterCacheState { RecentFilters = existingRecent }; var favorites = ImmutableList.Create(Constants.FilterIdEquals100); - var action = new FilterCacheAction.AddFavoriteFilterCompleted(favorites); + var action = new AddFavoriteFilterCompletedAction(favorites); // Act - var newState = FilterCacheReducers.ReduceAddFavoriteFilterCompleted(state, action); + var newState = Reducers.ReduceAddFavoriteFilterCompleted(state, action); // Assert Assert.Single(newState.FavoriteFilters); @@ -326,10 +326,10 @@ public void ReduceAddFavoriteFilterCompleted_ShouldUpdateFavorites() // Arrange var state = new FilterCacheState(); var filters = ImmutableList.Create(Constants.FilterIdEquals100, Constants.FilterIdEquals200); - var action = new FilterCacheAction.AddFavoriteFilterCompleted(filters); + var action = new AddFavoriteFilterCompletedAction(filters); // Act - var newState = FilterCacheReducers.ReduceAddFavoriteFilterCompleted(state, action); + var newState = Reducers.ReduceAddFavoriteFilterCompleted(state, action); // Assert Assert.Equal(2, newState.FavoriteFilters.Count); @@ -344,10 +344,10 @@ public void ReduceAddRecentFilterCompleted_ShouldNotAffectFavoriteFilters() var existingFavorites = ImmutableList.Create(Constants.FilterLevelEqualsError); var state = new FilterCacheState { FavoriteFilters = existingFavorites }; var recent = ImmutableQueue.Create(Constants.FilterIdEquals100); - var action = new FilterCacheAction.AddRecentFilterCompleted(recent); + var action = new AddRecentFilterCompletedAction(recent); // Act - var newState = FilterCacheReducers.ReduceAddRecentFilterCompleted(state, action); + var newState = Reducers.ReduceAddRecentFilterCompleted(state, action); // Assert Assert.Single(newState.RecentFilters); @@ -361,10 +361,10 @@ public void ReduceAddRecentFilterCompleted_ShouldUpdateRecent() // Arrange var state = new FilterCacheState(); var filters = ImmutableQueue.Create(Constants.FilterIdEquals100, Constants.FilterIdEquals200); - var action = new FilterCacheAction.AddRecentFilterCompleted(filters); + var action = new AddRecentFilterCompletedAction(filters); // Act - var newState = FilterCacheReducers.ReduceAddRecentFilterCompleted(state, action); + var newState = Reducers.ReduceAddRecentFilterCompleted(state, action); // Assert Assert.Equal(2, newState.RecentFilters.Count()); @@ -385,10 +385,10 @@ public void ReduceLoadFiltersCompleted_ShouldReplaceExistingFilters() var newFavorites = ImmutableList.Create(Constants.FilterIdEquals100); var newRecent = ImmutableQueue.Create(Constants.FilterLevelEqualsError); - var action = new FilterCacheAction.LoadFiltersCompleted(newFavorites, newRecent); + var action = new LoadFiltersCompletedAction(newFavorites, newRecent); // Act - var newState = FilterCacheReducers.ReduceLoadFiltersCompleted(state, action); + var newState = Reducers.ReduceLoadFiltersCompleted(state, action); // Assert Assert.Single(newState.FavoriteFilters); @@ -404,10 +404,10 @@ public void ReduceLoadFiltersCompleted_ShouldUpdateBothFilters() var state = new FilterCacheState(); var favorites = ImmutableList.Create(Constants.FilterIdEquals100, Constants.FilterIdEquals200); var recent = ImmutableQueue.Create(Constants.FilterLevelEqualsError); - var action = new FilterCacheAction.LoadFiltersCompleted(favorites, recent); + var action = new LoadFiltersCompletedAction(favorites, recent); // Act - var newState = FilterCacheReducers.ReduceLoadFiltersCompleted(state, action); + var newState = Reducers.ReduceLoadFiltersCompleted(state, action); // Assert Assert.Equal(2, newState.FavoriteFilters.Count); @@ -429,10 +429,10 @@ public void ReduceRemoveFavoriteFilterCompleted_ShouldUpdateBothFilters() var updatedFavorites = ImmutableList.Create(Constants.FilterIdEquals200); var updatedRecent = ImmutableQueue.Create(Constants.FilterLevelEqualsError, Constants.FilterIdEquals100); - var action = new FilterCacheAction.RemoveFavoriteFilterCompleted(updatedFavorites, updatedRecent); + var action = new RemoveFavoriteFilterCompletedAction(updatedFavorites, updatedRecent); // Act - var newState = FilterCacheReducers.ReduceRemoveFavoriteFilterCompleted(state, action); + var newState = Reducers.ReduceRemoveFavoriteFilterCompleted(state, action); // Assert Assert.Single(newState.FavoriteFilters); @@ -447,12 +447,12 @@ public void ReduceRemoveFavoriteFilterCompleted_WithEmptyFavorites_ShouldClearFa var favorites = ImmutableList.Create(Constants.FilterIdEquals100); var state = new FilterCacheState { FavoriteFilters = favorites }; - var action = new FilterCacheAction.RemoveFavoriteFilterCompleted( + var action = new RemoveFavoriteFilterCompletedAction( ImmutableList.Empty, ImmutableQueue.Create(Constants.FilterIdEquals100)); // Act - var newState = FilterCacheReducers.ReduceRemoveFavoriteFilterCompleted(state, action); + var newState = Reducers.ReduceRemoveFavoriteFilterCompleted(state, action); // Assert Assert.Empty(newState.FavoriteFilters); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs similarity index 90% rename from tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupEffectsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs index dba66077..a0a293b8 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs @@ -11,7 +11,7 @@ namespace EventLogExpert.UI.Tests.Store.FilterGroup; -public sealed class FilterGroupEffectsTests +public sealed class EffectsTests { [Fact] public async Task HandleAddGroup_ShouldPersistToPreferences() @@ -84,14 +84,14 @@ public async Task HandleLoadGroups_ShouldDispatchLoadGroupsSuccess() var mockState = Substitute.For>(); mockState.Value.Returns(new FilterGroupState()); - var effects = new FilterGroupEffects(mockState, mockPreferencesProvider); + var effects = new Effects(mockState, mockPreferencesProvider); var mockDispatcher = Substitute.For(); // Act await effects.HandleLoadGroups(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Groups.Count() == 2 && x.Groups.Any(g => g.Name == Constants.FilterGroupName) && x.Groups.Any(g => g.Name == Constants.FilterGroupNameNested))); @@ -107,14 +107,14 @@ public async Task HandleLoadGroups_WhenPreferencesEmpty_ShouldDispatchEmptyList( var mockState = Substitute.For>(); mockState.Value.Returns(new FilterGroupState()); - var effects = new FilterGroupEffects(mockState, mockPreferencesProvider); + var effects = new Effects(mockState, mockPreferencesProvider); var mockDispatcher = Substitute.For(); // Act await effects.HandleLoadGroups(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => !x.Groups.Any())); } @@ -177,7 +177,7 @@ public async Task HandleSetGroup_WithMultipleGroups_ShouldPersistAll() x.Count() == 3); } - private static (FilterGroupEffects effects, IDispatcher mockDispatcher, IPreferencesProvider mockPreferencesProvider) + private static (Effects effects, IDispatcher mockDispatcher, IPreferencesProvider mockPreferencesProvider) CreateEffects(List? groups = null) { var mockState = Substitute.For>(); @@ -189,7 +189,7 @@ private static (FilterGroupEffects effects, IDispatcher mockDispatcher, IPrefere var mockPreferencesProvider = Substitute.For(); - var effects = new FilterGroupEffects(mockState, mockPreferencesProvider); + var effects = new Effects(mockState, mockPreferencesProvider); var mockDispatcher = Substitute.For(); return (effects, mockDispatcher, mockPreferencesProvider); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupStoreTests.cs index 7178341b..ce65f389 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/FilterGroupStoreTests.cs @@ -17,7 +17,7 @@ public void FilterGroupAction_AddGroup_WithGroup_ShouldStoreGroup() var group = new FilterGroupModel { Name = Constants.FilterGroupName }; // Act - var action = new FilterGroupAction.AddGroup(group); + var action = new AddGroupAction(group); // Assert Assert.NotNull(action.FilterGroup); @@ -28,7 +28,7 @@ public void FilterGroupAction_AddGroup_WithGroup_ShouldStoreGroup() public void FilterGroupAction_AddGroup_WithNoGroup_ShouldStoreNull() { // Act - var action = new FilterGroupAction.AddGroup(); + var action = new AddGroupAction(); // Assert Assert.Null(action.FilterGroup); @@ -45,7 +45,7 @@ public void FilterGroupAction_ImportGroups_ShouldStoreGroups() }; // Act - var action = new FilterGroupAction.ImportGroups(groups); + var action = new ImportGroupsAction(groups); // Assert Assert.Equal(2, action.Groups.Count()); @@ -58,7 +58,7 @@ public void FilterGroupAction_LoadGroupsSuccess_ShouldStoreGroups() var groups = new List { new() { Name = Constants.FilterGroupName } }; // Act - var action = new FilterGroupAction.LoadGroupsSuccess(groups); + var action = new LoadGroupsSuccessAction(groups); // Assert Assert.Single(action.Groups); @@ -72,7 +72,7 @@ public void FilterGroupAction_RemoveFilter_ShouldStoreParentIdAndFilterId() var filterId = FilterId.Create(); // Act - var action = new FilterGroupAction.RemoveFilter(parentId, filterId); + var action = new RemoveFilterAction(parentId, filterId); // Assert Assert.Equal(parentId, action.ParentId); @@ -86,7 +86,7 @@ public void FilterGroupAction_RemoveGroup_ShouldStoreId() var groupId = FilterGroupId.Create(); // Act - var action = new FilterGroupAction.RemoveGroup(groupId); + var action = new RemoveGroupAction(groupId); // Assert Assert.Equal(groupId, action.Id); @@ -100,7 +100,7 @@ public void FilterGroupAction_SetFilter_ShouldStoreParentIdAndFilter() var filter = FilterUtils.CreateTestFilter(); // Act - var action = new FilterGroupAction.SetFilter(parentId, filter); + var action = new SetFilterAction(parentId, filter); // Assert Assert.Equal(parentId, action.ParentId); @@ -114,7 +114,7 @@ public void FilterGroupAction_SetGroup_ShouldStoreGroup() var group = new FilterGroupModel { Name = Constants.FilterGroupName }; // Act - var action = new FilterGroupAction.SetGroup(group); + var action = new SetGroupAction(group); // Assert Assert.Equal(group, action.FilterGroup); @@ -128,7 +128,7 @@ public void FilterGroupAction_ToggleFilterExcluded_ShouldStoreParentIdAndFilterI var filterId = FilterId.Create(); // Act - var action = new FilterGroupAction.ToggleFilterExcluded(parentId, filterId); + var action = new ToggleFilterExcludedAction(parentId, filterId); // Assert Assert.Equal(parentId, action.ParentId); @@ -142,7 +142,7 @@ public void FilterGroupAction_ToggleGroup_ShouldStoreId() var groupId = FilterGroupId.Create(); // Act - var action = new FilterGroupAction.ToggleGroup(groupId); + var action = new ToggleGroupAction(groupId); // Assert Assert.Equal(groupId, action.Id); @@ -202,7 +202,7 @@ public void IntegrationTest_AddGroupAndFilters() // Act - Add group var group = new FilterGroupModel { Name = Constants.FilterGroupName }; - state = FilterGroupReducers.ReducerAddGroup(state, new FilterGroupAction.AddGroup(group)); + state = Reducers.ReducerAddGroup(state, new AddGroupAction(group)); // Assert Assert.Single(state.Groups); @@ -211,7 +211,7 @@ public void IntegrationTest_AddGroupAndFilters() var groupId = state.Groups.First().Id; var filter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100); - state = FilterGroupReducers.ReducerSetFilter(state, new FilterGroupAction.SetFilter(groupId, filter)); + state = Reducers.ReducerSetFilter(state, new SetFilterAction(groupId, filter)); // Assert Assert.Single(state.Groups.First().Filters); @@ -225,27 +225,27 @@ public void IntegrationTest_CompleteGroupLifecycle() // Act - Add group var group = new FilterGroupModel { Name = Constants.FilterGroupName }; - state = FilterGroupReducers.ReducerAddGroup(state, new FilterGroupAction.AddGroup(group)); + state = Reducers.ReducerAddGroup(state, new AddGroupAction(group)); var groupId = state.Groups.First().Id; // Act - Upsert a new filter into the group (pending-draft commit path) var initialFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, color: HighlightColor.Blue); - state = FilterGroupReducers.ReducerSetFilter(state, new FilterGroupAction.SetFilter(groupId, initialFilter)); + state = Reducers.ReducerSetFilter(state, new SetFilterAction(groupId, initialFilter)); // Assert Assert.Equal(HighlightColor.Blue, state.Groups.First().Filters[0].Color); // Act - Remove filter var filterId = state.Groups.First().Filters[0].Id; - state = FilterGroupReducers.ReducerRemoveFilter(state, - new FilterGroupAction.RemoveFilter(groupId, filterId)); + state = Reducers.ReducerRemoveFilter(state, + new RemoveFilterAction(groupId, filterId)); // Assert Assert.Empty(state.Groups.First().Filters); // Act - Remove group - state = FilterGroupReducers.ReducerRemoveGroup(state, new FilterGroupAction.RemoveGroup(groupId)); + state = Reducers.ReducerRemoveGroup(state, new RemoveGroupAction(groupId)); // Assert Assert.Empty(state.Groups); @@ -265,9 +265,9 @@ public void IntegrationTest_DisplayGroupsGeneration() }; // Act - Load groups (DisplayGroups is now built atomically inside the reducer) - state = FilterGroupReducers.ReducerLoadGroupsSuccess( + state = Reducers.ReducerLoadGroupsSuccess( state, - new FilterGroupAction.LoadGroupsSuccess(groups)); + new LoadGroupsSuccessAction(groups)); // Assert Assert.NotEmpty(state.DisplayGroups); @@ -292,9 +292,9 @@ public void IntegrationTest_FilterManipulation() var state = new FilterGroupState { Groups = [group] }; // Act - Toggle excluded - state = FilterGroupReducers.ReducerToggleFilterExcluded( + state = Reducers.ReducerToggleFilterExcluded( state, - new FilterGroupAction.ToggleFilterExcluded(group.Id, filter.Id)); + new ToggleFilterExcludedAction(group.Id, filter.Id)); // Assert Assert.True(state.Groups.First().Filters[0].IsExcluded); @@ -305,9 +305,9 @@ public void IntegrationTest_FilterManipulation() Color = HighlightColor.Red }; - state = FilterGroupReducers.ReducerSetFilter( + state = Reducers.ReducerSetFilter( state, - new FilterGroupAction.SetFilter(group.Id, updatedFilter)); + new SetFilterAction(group.Id, updatedFilter)); // Assert Assert.Equal(HighlightColor.Red, state.Groups.First().Filters[0].Color); @@ -326,9 +326,9 @@ public void IntegrationTest_LoadAndModifyGroups() }; // Act - Load groups - state = FilterGroupReducers.ReducerLoadGroupsSuccess( + state = Reducers.ReducerLoadGroupsSuccess( state, - new FilterGroupAction.LoadGroupsSuccess(groups)); + new LoadGroupsSuccessAction(groups)); // Assert Assert.Equal(2, state.Groups.Count); @@ -336,9 +336,9 @@ public void IntegrationTest_LoadAndModifyGroups() // Act - Toggle group editing var firstGroupId = state.Groups.First().Id; - state = FilterGroupReducers.ReducerToggleGroup( + state = Reducers.ReducerToggleGroup( state, - new FilterGroupAction.ToggleGroup(firstGroupId)); + new ToggleGroupAction(firstGroupId)); // Assert Assert.True(state.Groups.First(g => g.Id == firstGroupId).IsEditing); @@ -350,10 +350,10 @@ public void ReducerAddGroup_ShouldRebuildDisplayGroupsAtomically() // Arrange var state = new FilterGroupState(); var group = new FilterGroupModel { Name = Constants.FilterGroupName }; - var action = new FilterGroupAction.AddGroup(group); + var action = new AddGroupAction(group); // Act - var newState = FilterGroupReducers.ReducerAddGroup(state, action); + var newState = Reducers.ReducerAddGroup(state, action); // Assert Assert.Single(newState.Groups); @@ -367,10 +367,10 @@ public void ReducerAddGroup_WithGroup_ShouldAddSpecifiedGroup() // Arrange var state = new FilterGroupState(); var group = new FilterGroupModel { Name = Constants.FilterGroupName }; - var action = new FilterGroupAction.AddGroup(group); + var action = new AddGroupAction(group); // Act - var newState = FilterGroupReducers.ReducerAddGroup(state, action); + var newState = Reducers.ReducerAddGroup(state, action); // Assert Assert.Single(newState.Groups); @@ -382,10 +382,10 @@ public void ReducerAddGroup_WithNoGroup_ShouldAddNewGroup() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.AddGroup(); + var action = new AddGroupAction(); // Act - var newState = FilterGroupReducers.ReducerAddGroup(state, action); + var newState = Reducers.ReducerAddGroup(state, action); // Assert Assert.Single(newState.Groups); @@ -404,10 +404,10 @@ public void ReducerImportGroups_ShouldAddMultipleGroups() new() { Name = "Another\\Group" } }; - var action = new FilterGroupAction.ImportGroups(newGroups); + var action = new ImportGroupsAction(newGroups); // Act - var newState = FilterGroupReducers.ReducerImportGroups(state, action); + var newState = Reducers.ReducerImportGroups(state, action); // Assert Assert.Equal(3, newState.Groups.Count); @@ -425,10 +425,10 @@ public void ReducerLoadGroupsSuccess_ShouldCreateDisplayHierarchy() new() { Name = Constants.FilterGroupNameNested } }; - var action = new FilterGroupAction.LoadGroupsSuccess(groups); + var action = new LoadGroupsSuccessAction(groups); // Act - var newState = FilterGroupReducers.ReducerLoadGroupsSuccess(state, action); + var newState = Reducers.ReducerLoadGroupsSuccess(state, action); // Assert Assert.NotEmpty(newState.DisplayGroups); @@ -448,10 +448,10 @@ public void ReducerLoadGroupsSuccess_ShouldReplaceAllGroups() new() { Name = Constants.FilterGroupNameNested } }; - var action = new FilterGroupAction.LoadGroupsSuccess(newGroups); + var action = new LoadGroupsSuccessAction(newGroups); // Act - var newState = FilterGroupReducers.ReducerLoadGroupsSuccess(state, action); + var newState = Reducers.ReducerLoadGroupsSuccess(state, action); // Assert Assert.Equal(2, newState.Groups.Count); @@ -463,10 +463,10 @@ public void ReducerLoadGroupsSuccess_WithEmptyGroups_ShouldHaveEmptyDisplay() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.LoadGroupsSuccess([]); + var action = new LoadGroupsSuccessAction([]); // Act - var newState = FilterGroupReducers.ReducerLoadGroupsSuccess(state, action); + var newState = Reducers.ReducerLoadGroupsSuccess(state, action); // Assert Assert.Empty(newState.DisplayGroups); @@ -485,10 +485,10 @@ public void ReducerRemoveFilter_ShouldRemoveFilterFromGroup() }; var state = new FilterGroupState { Groups = [group] }; - var action = new FilterGroupAction.RemoveFilter(group.Id, filter.Id); + var action = new RemoveFilterAction(group.Id, filter.Id); // Act - var newState = FilterGroupReducers.ReducerRemoveFilter(state, action); + var newState = Reducers.ReducerRemoveFilter(state, action); // Assert var updatedGroup = newState.Groups.First(g => g.Id == group.Id); @@ -501,10 +501,10 @@ public void ReducerRemoveFilter_WhenFilterNotFound_ShouldReturnSameState() // Arrange var group = new FilterGroupModel { Name = Constants.FilterGroupName }; var state = new FilterGroupState { Groups = [group] }; - var action = new FilterGroupAction.RemoveFilter(group.Id, FilterId.Create()); + var action = new RemoveFilterAction(group.Id, FilterId.Create()); // Act - var newState = FilterGroupReducers.ReducerRemoveFilter(state, action); + var newState = Reducers.ReducerRemoveFilter(state, action); // Assert Assert.Same(state, newState); @@ -515,10 +515,10 @@ public void ReducerRemoveFilter_WhenGroupNotFound_ShouldReturnSameState() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.RemoveFilter(FilterGroupId.Create(), FilterId.Create()); + var action = new RemoveFilterAction(FilterGroupId.Create(), FilterId.Create()); // Act - var newState = FilterGroupReducers.ReducerRemoveFilter(state, action); + var newState = Reducers.ReducerRemoveFilter(state, action); // Assert Assert.Same(state, newState); @@ -529,12 +529,12 @@ public void ReducerRemoveGroup_ShouldRebuildDisplayGroupsAtomically() { // Arrange var group = new FilterGroupModel { Name = Constants.FilterGroupName }; - var state = FilterGroupReducers.ReducerAddGroup( + var state = Reducers.ReducerAddGroup( new FilterGroupState(), - new FilterGroupAction.AddGroup(group)); + new AddGroupAction(group)); // Act - var newState = FilterGroupReducers.ReducerRemoveGroup(state, new FilterGroupAction.RemoveGroup(group.Id)); + var newState = Reducers.ReducerRemoveGroup(state, new RemoveGroupAction(group.Id)); // Assert Assert.Empty(newState.Groups); @@ -548,10 +548,10 @@ public void ReducerRemoveGroup_ShouldRemoveGroup() var group1 = new FilterGroupModel { Name = Constants.FilterGroupName }; var group2 = new FilterGroupModel { Name = Constants.FilterGroupNameNested }; var state = new FilterGroupState { Groups = [group1, group2] }; - var action = new FilterGroupAction.RemoveGroup(group1.Id); + var action = new RemoveGroupAction(group1.Id); // Act - var newState = FilterGroupReducers.ReducerRemoveGroup(state, action); + var newState = Reducers.ReducerRemoveGroup(state, action); // Assert Assert.Single(newState.Groups); @@ -563,10 +563,10 @@ public void ReducerRemoveGroup_WhenGroupNotFound_ShouldReturnSameState() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.RemoveGroup(FilterGroupId.Create()); + var action = new RemoveGroupAction(FilterGroupId.Create()); // Act - var newState = FilterGroupReducers.ReducerRemoveGroup(state, action); + var newState = Reducers.ReducerRemoveGroup(state, action); // Assert Assert.Same(state, newState); @@ -591,10 +591,10 @@ public void ReducerSetFilter_ShouldUpdateFilter() color: HighlightColor.Green, id: filter.Id); - var action = new FilterGroupAction.SetFilter(group.Id, updatedFilter); + var action = new SetFilterAction(group.Id, updatedFilter); // Act - var newState = FilterGroupReducers.ReducerSetFilter(state, action); + var newState = Reducers.ReducerSetFilter(state, action); // Assert var updatedGroup = newState.Groups.First(g => g.Id == group.Id); @@ -610,10 +610,10 @@ public void ReducerSetFilter_WhenFilterNotFound_ShouldAppendFilter() var group = new FilterGroupModel { Name = Constants.FilterGroupName }; var state = new FilterGroupState { Groups = [group] }; var newFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, color: HighlightColor.Yellow); - var action = new FilterGroupAction.SetFilter(group.Id, newFilter); + var action = new SetFilterAction(group.Id, newFilter); // Act - var newState = FilterGroupReducers.ReducerSetFilter(state, action); + var newState = Reducers.ReducerSetFilter(state, action); // Assert var resultGroup = newState.Groups.First(g => g.Id == group.Id); @@ -628,10 +628,10 @@ public void ReducerSetFilter_WhenGroupNotFound_ShouldReturnSameState() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.SetFilter(FilterGroupId.Create(), FilterUtils.CreateTestFilter()); + var action = new SetFilterAction(FilterGroupId.Create(), FilterUtils.CreateTestFilter()); // Act - var newState = FilterGroupReducers.ReducerSetFilter(state, action); + var newState = Reducers.ReducerSetFilter(state, action); // Assert Assert.Same(state, newState); @@ -650,10 +650,10 @@ public void ReducerSetGroup_ShouldUpdateGroup() Filters = [FilterUtils.CreateTestFilter()] }; - var action = new FilterGroupAction.SetGroup(updatedGroup); + var action = new SetGroupAction(updatedGroup); // Act - var newState = FilterGroupReducers.ReducerSetGroup(state, action); + var newState = Reducers.ReducerSetGroup(state, action); // Assert var resultGroup = newState.Groups.First(g => g.Id == group.Id); @@ -667,10 +667,10 @@ public void ReducerSetGroup_WhenGroupNotFound_ShouldReturnSameState() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.SetGroup(new FilterGroupModel()); + var action = new SetGroupAction(new FilterGroupModel()); // Act - var newState = FilterGroupReducers.ReducerSetGroup(state, action); + var newState = Reducers.ReducerSetGroup(state, action); // Assert Assert.Same(state, newState); @@ -689,10 +689,10 @@ public void ReducerToggleFilterExcluded_ShouldToggleIsExcluded() }; var state = new FilterGroupState { Groups = [group] }; - var action = new FilterGroupAction.ToggleFilterExcluded(group.Id, filter.Id); + var action = new ToggleFilterExcludedAction(group.Id, filter.Id); // Act - var newState = FilterGroupReducers.ReducerToggleFilterExcluded(state, action); + var newState = Reducers.ReducerToggleFilterExcluded(state, action); // Assert var updatedGroup = newState.Groups.First(g => g.Id == group.Id); @@ -705,10 +705,10 @@ public void ReducerToggleFilterExcluded_WhenFilterNotFound_ShouldReturnSameState // Arrange var group = new FilterGroupModel { Name = Constants.FilterGroupName }; var state = new FilterGroupState { Groups = [group] }; - var action = new FilterGroupAction.ToggleFilterExcluded(group.Id, FilterId.Create()); + var action = new ToggleFilterExcludedAction(group.Id, FilterId.Create()); // Act - var newState = FilterGroupReducers.ReducerToggleFilterExcluded(state, action); + var newState = Reducers.ReducerToggleFilterExcluded(state, action); // Assert Assert.Same(state, newState); @@ -719,10 +719,10 @@ public void ReducerToggleFilterExcluded_WhenGroupNotFound_ShouldReturnSameState( { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.ToggleFilterExcluded(FilterGroupId.Create(), FilterId.Create()); + var action = new ToggleFilterExcludedAction(FilterGroupId.Create(), FilterId.Create()); // Act - var newState = FilterGroupReducers.ReducerToggleFilterExcluded(state, action); + var newState = Reducers.ReducerToggleFilterExcluded(state, action); // Assert Assert.Same(state, newState); @@ -734,10 +734,10 @@ public void ReducerToggleGroup_ShouldToggleIsEditing() // Arrange var group = new FilterGroupModel { Name = Constants.FilterGroupName, IsEditing = false }; var state = new FilterGroupState { Groups = [group] }; - var action = new FilterGroupAction.ToggleGroup(group.Id); + var action = new ToggleGroupAction(group.Id); // Act - var newState = FilterGroupReducers.ReducerToggleGroup(state, action); + var newState = Reducers.ReducerToggleGroup(state, action); // Assert var updatedGroup = newState.Groups.First(g => g.Id == group.Id); @@ -749,10 +749,10 @@ public void ReducerToggleGroup_WhenGroupNotFound_ShouldReturnSameState() { // Arrange var state = new FilterGroupState(); - var action = new FilterGroupAction.ToggleGroup(FilterGroupId.Create()); + var action = new ToggleGroupAction(FilterGroupId.Create()); // Act - var newState = FilterGroupReducers.ReducerToggleGroup(state, action); + var newState = Reducers.ReducerToggleGroup(state, action); // Assert Assert.Same(state, newState); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/EffectsTests.cs similarity index 80% rename from tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/EffectsTests.cs index a12ff6b9..184a73ec 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneEffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/EffectsTests.cs @@ -13,10 +13,12 @@ using Fluxor; using NSubstitute; using System.Collections.Immutable; +using Effects = EventLogExpert.UI.Store.FilterPane.Effects; +using SetFilterAction = EventLogExpert.UI.Store.FilterPane.SetFilterAction; namespace EventLogExpert.UI.Tests.Store.FilterPane; -public sealed class FilterPaneEffectsTests +public sealed class EffectsTests { [Fact] public async Task HandleAddFilter_WhenComparisonValueExists_ShouldUpdateEventTableFilters() @@ -25,14 +27,14 @@ public async Task HandleAddFilter_WhenComparisonValueExists_ShouldUpdateEventTab var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); var (effects, mockDispatcher) = CreateEffects(true, ImmutableList.Create(filterModel)); - var action = new FilterPaneAction.AddFilter(filterModel); + var action = new AddFilterAction(filterModel); // Act await effects.HandleAddFilter(action, mockDispatcher); // Assert - mockDispatcher.Received().Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.Received().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -47,13 +49,13 @@ public async Task HandleAddFilter_WhenComparisonValueIsNull_ShouldNotUpdateEvent }; var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.AddFilter(filterModel); + var action = new AddFilterAction(filterModel); // Act await effects.HandleAddFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -63,13 +65,13 @@ public async Task HandleAddFilter_WhenFilterIsCached_ShouldNotAddToRecentFilters var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Cached); var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.AddFilter(filterModel); + var action = new AddFilterAction(filterModel); // Act await effects.HandleAddFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -79,13 +81,13 @@ public async Task HandleAddFilter_WhenFilterIsNotCached_ShouldAddToRecentFilters var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Advanced); var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.AddFilter(filterModel); + var action = new AddFilterAction(filterModel); // Act await effects.HandleAddFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Filter == Constants.FilterIdEquals100)); } @@ -99,7 +101,7 @@ public async Task HandleApplyFilterGroup_ShouldUpdateEventTableFilters() await effects.HandleApplyFilterGroup(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -114,7 +116,7 @@ public async Task HandleClearAllFilters_ShouldUpdateEventTableFilters() await effects.HandleClearAllFilters(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -129,7 +131,7 @@ public async Task HandleRemoveAdvancedFilter_ShouldUpdateEventTableFilters() await effects.HandleRemoveAdvancedFilter(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -143,13 +145,13 @@ public async Task HandleSaveFilterGroup_ShouldCopyFilterProperties() isExcluded: true)); var (effects, mockDispatcher) = CreateEffects(filters: filters); - var action = new FilterPaneAction.SaveFilterGroup(Constants.FilterGroupName); + var action = new SaveFilterGroupAction(Constants.FilterGroupName); // Act await effects.HandleSaveFilterGroup(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterGroup != null && x.FilterGroup.Filters[0].Color == HighlightColor.Red && x.FilterGroup.Filters[0].IsExcluded == true && @@ -167,13 +169,13 @@ public async Task HandleSaveFilterGroup_ShouldDispatchAddGroupAction() isExcluded: false)); var (effects, mockDispatcher) = CreateEffects(filters: filters); - var action = new FilterPaneAction.SaveFilterGroup(Constants.FilterGroupName); + var action = new SaveFilterGroupAction(Constants.FilterGroupName); // Act await effects.HandleSaveFilterGroup(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterGroup != null && x.FilterGroup.Name == Constants.FilterGroupName && x.FilterGroup.Filters.Count == 1)); @@ -188,13 +190,13 @@ public async Task HandleSaveFilterGroup_WithMultipleFilters_ShouldSaveAll() FilterUtils.CreateTestFilter(Constants.FilterLevelEqualsError, color: HighlightColor.Red)); var (effects, mockDispatcher) = CreateEffects(filters: filters); - var action = new FilterPaneAction.SaveFilterGroup(Constants.FilterGroupName); + var action = new SaveFilterGroupAction(Constants.FilterGroupName); // Act await effects.HandleSaveFilterGroup(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterGroup != null && x.FilterGroup.Filters.Count == 2)); } @@ -203,19 +205,16 @@ public async Task HandleSaveFilterGroup_WithMultipleFilters_ShouldSaveAll() public async Task HandleSetFilterDateRangeSuccess_ShouldUpdateEventTableFilters() { // Arrange - var (effects, mockDispatcher) = CreateEffects( - isEnabled: true, - filteredDateRange: new FilterDateModel - { - After = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc), - Before = new DateTime(2024, 1, 2, 0, 0, 0, DateTimeKind.Utc) - }); + var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); + + var (effects, mockDispatcher) = CreateEffects(true, ImmutableList.Create(filterModel)); + var action = new SetFilterAction(filterModel); // Act await effects.HandleSetFilterDateRangeSuccess(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -236,14 +235,14 @@ public async Task HandleSetFilterDateRange_WhenAfterIsNull_ShouldUseEnvelopeFrom var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects(activeLogs: activeLogs); - var action = new FilterPaneAction.SetFilterDateRange(new FilterDateModel { Before = unrelatedBefore }); + var action = new SetFilterDateRangeAction(new FilterDateModel { Before = unrelatedBefore }); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert var expectedAfter = new DateTime(2024, 1, 1, 8, 0, 0, DateTimeKind.Utc); - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel != null && x.FilterDateModel.After == expectedAfter && x.FilterDateModel.Before == unrelatedBefore)); @@ -267,14 +266,14 @@ public async Task HandleSetFilterDateRange_WhenBeforeIsNull_ShouldUseEnvelopeFro var activeLogs = ImmutableDictionary.Empty.Add(Constants.LogNameTestLog, logData); var (effects, mockDispatcher) = CreateEffects(activeLogs: activeLogs); - var action = new FilterPaneAction.SetFilterDateRange(new FilterDateModel { After = unrelatedAfter }); + var action = new SetFilterDateRangeAction(new FilterDateModel { After = unrelatedAfter }); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert var expectedBefore = new DateTime(2024, 1, 1, 15, 0, 0, DateTimeKind.Utc); - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel != null && x.FilterDateModel.After == unrelatedAfter && x.FilterDateModel.Before == expectedBefore)); @@ -304,13 +303,13 @@ public async Task HandleSetFilterDateRange_WhenBothNullAcrossMultipleLogs_Should .Add("LogB", logB); var (effects, mockDispatcher) = CreateEffects(activeLogs: activeLogs); - var action = new FilterPaneAction.SetFilterDateRange(new FilterDateModel()); + var action = new SetFilterDateRangeAction(new FilterDateModel()); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel != null && x.FilterDateModel.After == new DateTime(2024, 1, 1, 4, 0, 0, DateTimeKind.Utc) && x.FilterDateModel.Before == new DateTime(2024, 1, 5, 22, 0, 0, DateTimeKind.Utc))); @@ -324,13 +323,13 @@ public async Task HandleSetFilterDateRange_WhenBothProvided_ShouldUseProvidedVal var before = new DateTime(2024, 1, 1, 14, 0, 0, DateTimeKind.Utc); var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.SetFilterDateRange(new FilterDateModel { After = after, Before = before }); + var action = new SetFilterDateRangeAction(new FilterDateModel { After = after, Before = before }); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel != null && x.FilterDateModel.After == after && x.FilterDateModel.Before == before)); @@ -346,13 +345,13 @@ public async Task HandleSetFilterDateRange_WhenExistingDateRangeHasAfter_ShouldU var (effects, mockDispatcher) = CreateEffects( filteredDateRange: new FilterDateModel { After = existingAfter }); - var action = new FilterPaneAction.SetFilterDateRange(new FilterDateModel { Before = newBefore }); + var action = new SetFilterDateRangeAction(new FilterDateModel { Before = newBefore }); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel != null && x.FilterDateModel.After == existingAfter && x.FilterDateModel.Before == newBefore)); @@ -368,13 +367,13 @@ public async Task HandleSetFilterDateRange_WhenExistingDateRangeHasBefore_Should var (effects, mockDispatcher) = CreateEffects( filteredDateRange: new FilterDateModel { Before = existingBefore }); - var action = new FilterPaneAction.SetFilterDateRange(new FilterDateModel { After = newAfter }); + var action = new SetFilterDateRangeAction(new FilterDateModel { After = newAfter }); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel != null && x.FilterDateModel.After == newAfter && x.FilterDateModel.Before == existingBefore)); @@ -385,13 +384,13 @@ public async Task HandleSetFilterDateRange_WhenFilterDateModelIsNull_ShouldDispa { // Arrange var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.SetFilterDateRange(null); + var action = new SetFilterDateRangeAction(null); // Act await effects.HandleSetFilterDateRange(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.FilterDateModel == null)); } @@ -402,13 +401,13 @@ public async Task HandleSetFilter_ShouldUpdateEventTableFilters() var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, isEnabled: true); var (effects, mockDispatcher) = CreateEffects(true, ImmutableList.Create(filterModel)); - var action = new FilterPaneAction.SetFilter(filterModel); + var action = new SetFilterAction(filterModel); // Act await effects.HandleSetFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -418,13 +417,13 @@ public async Task HandleSetFilter_WhenFilterIsCached_ShouldNotAddToRecentFilters var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Cached); var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.SetFilter(filterModel); + var action = new SetFilterAction(filterModel); // Act await effects.HandleSetFilter(action, mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -434,13 +433,13 @@ public async Task HandleSetFilter_WhenFilterIsNotCached_ShouldAddToRecentFilters var filterModel = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100, FilterType.Advanced); var (effects, mockDispatcher) = CreateEffects(); - var action = new FilterPaneAction.SetFilter(filterModel); + var action = new SetFilterAction(filterModel); // Act await effects.HandleSetFilter(action, mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.Filter == Constants.FilterIdEquals100)); } @@ -460,7 +459,7 @@ public async Task HandleToggleFilterDate_ShouldUpdateEventTableFilters() await effects.HandleToggleFilterDate(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -473,7 +472,7 @@ public async Task HandleToggleFilterEnabled_ShouldUpdateEventTableFilters() await effects.HandleToggleFilterEnabled(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -486,7 +485,7 @@ public async Task HandleToggleFilterExcluded_ShouldUpdateEventTableFilters() await effects.HandleToggleFilterExcluded(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -499,7 +498,7 @@ public async Task HandleToggleIsEnabled_ShouldUpdateEventTableFilters() await effects.HandleToggleIsEnabled(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] @@ -527,7 +526,7 @@ public async Task UpdateEventTableFilters_WhenEquivalentFiltersFromDifferentInst await effects.HandleToggleIsEnabled(mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } [Fact] @@ -550,7 +549,7 @@ public async Task UpdateEventTableFilters_WhenFilterPaneDisabled_ShouldOnlyKeepE await effects.HandleToggleIsEnabled(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.EventFilter.Filters.Count == 1 && x.EventFilter.Filters[0].IsExcluded)); } @@ -575,7 +574,7 @@ public async Task UpdateEventTableFilters_WhenFilterPaneEnabled_ShouldIncludeEna await effects.HandleToggleIsEnabled(mockDispatcher); // Assert - mockDispatcher.Received(1).Dispatch(Arg.Is(x => + mockDispatcher.Received(1).Dispatch(Arg.Is(x => x.EventFilter.Filters.Count == 1 && x.EventFilter.Filters[0].ComparisonText == Constants.FilterIdEquals100)); } @@ -594,11 +593,11 @@ public async Task UpdateEventTableFilters_WhenFilterUnchanged_ShouldNotDispatch( await effects.HandleToggleIsEnabled(mockDispatcher); // Assert - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); - mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); + mockDispatcher.DidNotReceive().Dispatch(Arg.Any()); } - private static (FilterPaneEffects effects, IDispatcher mockDispatcher) CreateEffects( + private static (Effects effects, IDispatcher mockDispatcher) CreateEffects( bool isEnabled = false, ImmutableList? filters = null, FilterDateModel? filteredDateRange = null, @@ -622,7 +621,7 @@ private static (FilterPaneEffects effects, IDispatcher mockDispatcher) CreateEff AppliedFilter = appliedFilter ?? new EventFilter(null, []) }); - var effects = new FilterPaneEffects(mockEventLogState, mockFilterPaneState); + var effects = new Effects(mockEventLogState, mockFilterPaneState); var mockDispatcher = Substitute.For(); return (effects, mockDispatcher); diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneStoreTests.cs index 08483a71..ed9d4eb2 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterPane/FilterPaneStoreTests.cs @@ -58,7 +58,7 @@ public sealed class FilterPaneActionTests public void AddFilterAction_WithFilter_ShouldCreateAction() { var filter = FilterUtils.CreateTestFilter(); - var action = new FilterPaneAction.AddFilter(filter); + var action = new AddFilterAction(filter); Assert.Equal(filter, action.FilterModel); } @@ -67,7 +67,7 @@ public void AddFilterAction_WithFilter_ShouldCreateAction() public void ApplyFilterGroupAction_ShouldCreateAction() { var filterGroup = new FilterGroupModel { Name = Constants.FilterGroupName }; - var action = new FilterPaneAction.ApplyFilterGroup(filterGroup); + var action = new ApplyFilterGroupAction(filterGroup); Assert.Equal(filterGroup, action.FilterGroup); } @@ -75,7 +75,7 @@ public void ApplyFilterGroupAction_ShouldCreateAction() [Fact] public void ClearAllFiltersAction_ShouldCreateAction() { - var action = new FilterPaneAction.ClearAllFilters(); + var action = new ClearAllFiltersAction(); Assert.NotNull(action); } @@ -84,7 +84,7 @@ public void ClearAllFiltersAction_ShouldCreateAction() public void RemoveFilterAction_ShouldCreateAction() { var filterId = FilterId.Create(); - var action = new FilterPaneAction.RemoveFilter(filterId); + var action = new RemoveFilterAction(filterId); Assert.Equal(filterId, action.Id); } @@ -92,7 +92,7 @@ public void RemoveFilterAction_ShouldCreateAction() [Fact] public void SaveFilterGroupAction_ShouldCreateAction() { - var action = new FilterPaneAction.SaveFilterGroup(Constants.FilterGroupName); + var action = new SaveFilterGroupAction(Constants.FilterGroupName); Assert.Equal(Constants.FilterGroupName, action.Name); } @@ -101,7 +101,7 @@ public void SaveFilterGroupAction_ShouldCreateAction() public void SetFilterAction_ShouldCreateAction() { var filter = FilterUtils.CreateTestFilter(); - var action = new FilterPaneAction.SetFilter(filter); + var action = new SetFilterAction(filter); Assert.Equal(filter, action.FilterModel); } @@ -110,7 +110,7 @@ public void SetFilterAction_ShouldCreateAction() public void SetFilterDateRangeAction_ShouldCreateAction() { var dateModel = new FilterDateModel { After = DateTime.UtcNow }; - var action = new FilterPaneAction.SetFilterDateRange(dateModel); + var action = new SetFilterDateRangeAction(dateModel); Assert.Equal(dateModel, action.FilterDateModel); } @@ -119,7 +119,7 @@ public void SetFilterDateRangeAction_ShouldCreateAction() public void SetFilterDateRangeSuccessAction_ShouldCreateAction() { var dateModel = new FilterDateModel { Before = DateTime.UtcNow }; - var action = new FilterPaneAction.SetFilterDateRangeSuccess(dateModel); + var action = new SetFilterDateRangeSuccessAction(dateModel); Assert.Equal(dateModel, action.FilterDateModel); } @@ -127,7 +127,7 @@ public void SetFilterDateRangeSuccessAction_ShouldCreateAction() [Fact] public void SetIsLoadingAction_ShouldCreateAction() { - var action = new FilterPaneAction.SetIsLoading(true); + var action = new SetIsLoadingAction(true); Assert.NotNull(action); Assert.True(action.IsLoading); @@ -136,7 +136,7 @@ public void SetIsLoadingAction_ShouldCreateAction() [Fact] public void ToggleFilterDateAction_ShouldCreateAction() { - var action = new FilterPaneAction.ToggleFilterDate(); + var action = new ToggleFilterDateAction(); Assert.NotNull(action); } @@ -145,7 +145,7 @@ public void ToggleFilterDateAction_ShouldCreateAction() public void ToggleFilterEnabledAction_ShouldCreateAction() { var filterId = FilterId.Create(); - var action = new FilterPaneAction.ToggleFilterEnabled(filterId); + var action = new ToggleFilterEnabledAction(filterId); Assert.Equal(filterId, action.Id); } @@ -154,7 +154,7 @@ public void ToggleFilterEnabledAction_ShouldCreateAction() public void ToggleFilterExcludedAction_ShouldCreateAction() { var filterId = FilterId.Create(); - var action = new FilterPaneAction.ToggleFilterExcluded(filterId); + var action = new ToggleFilterExcludedAction(filterId); Assert.Equal(filterId, action.Id); } @@ -162,7 +162,7 @@ public void ToggleFilterExcludedAction_ShouldCreateAction() [Fact] public void ToggleIsEnabledAction_ShouldCreateAction() { - var action = new FilterPaneAction.ToggleIsEnabled(); + var action = new ToggleIsEnabledAction(); Assert.NotNull(action); } @@ -175,9 +175,9 @@ public void ReduceAddFilter_ShouldNotModifyOriginalState() { var state = new FilterPaneState(); var filter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100); - var action = new FilterPaneAction.AddFilter(filter); + var action = new AddFilterAction(filter); - FilterPaneReducers.ReduceAddFilter(state, action); + Reducers.ReduceAddFilter(state, action); Assert.Empty(state.Filters); } @@ -187,9 +187,9 @@ public void ReduceAddFilter_WithFilter_ShouldAddFilter() { var state = new FilterPaneState(); var filter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100); - var action = new FilterPaneAction.AddFilter(filter); + var action = new AddFilterAction(filter); - var result = FilterPaneReducers.ReduceAddFilter(state, action); + var result = Reducers.ReduceAddFilter(state, action); Assert.Single(result.Filters); Assert.Equal(filter, result.Filters[0]); @@ -222,9 +222,9 @@ public void ReduceApplyFilterGroup_PreservesBasicFilterAndFilterType() ] }; - var result = FilterPaneReducers.ReduceApplyFilterGroup( + var result = Reducers.ReduceApplyFilterGroup( state, - new FilterPaneAction.ApplyFilterGroup(filterGroup)); + new ApplyFilterGroupAction(filterGroup)); Assert.Single(result.Filters); Assert.Equal(FilterType.Basic, result.Filters[0].FilterType); @@ -246,9 +246,9 @@ public void ReduceApplyFilterGroup_ShouldPreserveIsExcludedOnAppliedFilters() ] }; - var action = new FilterPaneAction.ApplyFilterGroup(filterGroup); + var action = new ApplyFilterGroupAction(filterGroup); - var result = FilterPaneReducers.ReduceApplyFilterGroup(state, action); + var result = Reducers.ReduceApplyFilterGroup(state, action); Assert.Single(result.Filters); Assert.True(result.Filters[0].IsExcluded); @@ -267,9 +267,9 @@ public void ReduceApplyFilterGroup_WithDuplicateFilter_ShouldSkipDuplicate() Filters = [FilterUtils.CreateTestFilter(Constants.FilterIdEquals100)] }; - var action = new FilterPaneAction.ApplyFilterGroup(filterGroup); + var action = new ApplyFilterGroupAction(filterGroup); - var result = FilterPaneReducers.ReduceApplyFilterGroup(state, action); + var result = Reducers.ReduceApplyFilterGroup(state, action); Assert.Single(result.Filters); } @@ -279,9 +279,9 @@ public void ReduceApplyFilterGroup_WithEmptyFilters_ShouldReturnOriginalState() { var state = new FilterPaneState(); var filterGroup = new FilterGroupModel { Filters = [] }; - var action = new FilterPaneAction.ApplyFilterGroup(filterGroup); + var action = new ApplyFilterGroupAction(filterGroup); - var result = FilterPaneReducers.ReduceApplyFilterGroup(state, action); + var result = Reducers.ReduceApplyFilterGroup(state, action); Assert.Equal(state, result); } @@ -299,9 +299,9 @@ public void ReduceApplyFilterGroup_WithNewFilters_ShouldAddFilters() ] }; - var action = new FilterPaneAction.ApplyFilterGroup(filterGroup); + var action = new ApplyFilterGroupAction(filterGroup); - var result = FilterPaneReducers.ReduceApplyFilterGroup(state, action); + var result = Reducers.ReduceApplyFilterGroup(state, action); Assert.Single(result.Filters); Assert.Equal(Constants.FilterIdEquals100, result.Filters[0].ComparisonText); @@ -325,9 +325,9 @@ public void ReduceApplyFilterGroup_WithSameComparisonButDifferentExclusion_Shoul ] }; - var action = new FilterPaneAction.ApplyFilterGroup(filterGroup); + var action = new ApplyFilterGroupAction(filterGroup); - var result = FilterPaneReducers.ReduceApplyFilterGroup(state, action); + var result = Reducers.ReduceApplyFilterGroup(state, action); Assert.Equal(2, result.Filters.Count); Assert.False(result.Filters[0].IsExcluded); @@ -343,7 +343,7 @@ public void ReduceClearFilters_ShouldClearFiltersButPreserveIsEnabled() IsEnabled = false }; - var result = FilterPaneReducers.ReduceClearFilters(state); + var result = Reducers.ReduceClearFilters(state); Assert.Empty(result.Filters); Assert.False(result.IsEnabled); @@ -353,9 +353,9 @@ public void ReduceClearFilters_ShouldClearFiltersButPreserveIsEnabled() public void ReduceRemoveFilter_WithInvalidFilter_ShouldReturnOriginalState() { var state = new FilterPaneState { Filters = [FilterUtils.CreateTestFilter()] }; - var action = new FilterPaneAction.RemoveFilter(FilterId.Create()); + var action = new RemoveFilterAction(FilterId.Create()); - var result = FilterPaneReducers.ReduceRemoveFilter(state, action); + var result = Reducers.ReduceRemoveFilter(state, action); Assert.Equal(state, result); } @@ -365,13 +365,47 @@ public void ReduceRemoveFilter_WithValidFilter_ShouldRemoveFilter() { var filter = FilterUtils.CreateTestFilter(); var state = new FilterPaneState { Filters = [filter] }; - var action = new FilterPaneAction.RemoveFilter(filter.Id); + var action = new RemoveFilterAction(filter.Id); - var result = FilterPaneReducers.ReduceRemoveFilter(state, action); + var result = Reducers.ReduceRemoveFilter(state, action); Assert.Empty(result.Filters); } + [Fact] + public void ReduceSetFilterDateRangeSuccess_ShouldSetDateRange() + { + var state = new FilterPaneState(); + + var dateModel = new FilterDateModel + { + After = DateTime.UtcNow.AddDays(-1), + Before = DateTime.UtcNow + }; + + var action = new SetFilterDateRangeSuccessAction(dateModel); + + var result = Reducers.ReduceSetFilterDateRangeSuccess(state, action); + + Assert.NotNull(result.FilteredDateRange); + Assert.Equal(dateModel, result.FilteredDateRange); + } + + [Fact] + public void ReduceSetFilterDateRangeSuccess_WithNull_ShouldSetNullDateRange() + { + var state = new FilterPaneState + { + FilteredDateRange = new FilterDateModel { After = DateTime.UtcNow } + }; + + var action = new SetFilterDateRangeSuccessAction(null); + + var result = Reducers.ReduceSetFilterDateRangeSuccess(state, action); + + Assert.Null(result.FilteredDateRange); + } + [Fact] public void ReduceSetFilter_ShouldReplaceFilter() { @@ -381,9 +415,9 @@ public void ReduceSetFilter_ShouldReplaceFilter() var updatedFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals200, id: originalFilter.Id); - var action = new FilterPaneAction.SetFilter(updatedFilter); + var action = new SetFilterAction(updatedFilter); - var result = FilterPaneReducers.ReduceSetFilter(state, action); + var result = Reducers.ReduceSetFilter(state, action); Assert.Single(result.Filters); Assert.Equal(Constants.FilterIdEquals200, result.Filters[0].ComparisonText); @@ -397,9 +431,9 @@ public void ReduceSetFilter_WhenIdFound_ShouldNotDuplicate() var replacement = FilterUtils.CreateTestFilter(Constants.FilterIdEquals200, id: existing.Id); - var action = new FilterPaneAction.SetFilter(replacement); + var action = new SetFilterAction(replacement); - var result = FilterPaneReducers.ReduceSetFilter(state, action); + var result = Reducers.ReduceSetFilter(state, action); Assert.Single(result.Filters); Assert.Equal(existing.Id, result.Filters[0].Id); @@ -418,9 +452,9 @@ public void ReduceSetFilter_WhenIdFound_ShouldReplaceInPlaceWithoutReordering() var replacement = FilterUtils.CreateTestFilter(Constants.FilterIdGreaterThan100, id: middle.Id); - var action = new FilterPaneAction.SetFilter(replacement); + var action = new SetFilterAction(replacement); - var result = FilterPaneReducers.ReduceSetFilter(state, action); + var result = Reducers.ReduceSetFilter(state, action); Assert.Equal(3, result.Filters.Count); Assert.Equal(Constants.FilterIdEquals100, result.Filters[0].ComparisonText); @@ -436,55 +470,21 @@ public void ReduceSetFilter_WhenIdNotFound_ShouldAppend() var state = new FilterPaneState { Filters = [existing] }; var newFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals200); - var action = new FilterPaneAction.SetFilter(newFilter); + var action = new SetFilterAction(newFilter); - var result = FilterPaneReducers.ReduceSetFilter(state, action); + var result = Reducers.ReduceSetFilter(state, action); Assert.Equal(2, result.Filters.Count); Assert.Equal(Constants.FilterIdEquals100, result.Filters[0].ComparisonText); Assert.Equal(Constants.FilterIdEquals200, result.Filters[1].ComparisonText); } - [Fact] - public void ReduceSetFilterDateRangeSuccess_ShouldSetDateRange() - { - var state = new FilterPaneState(); - - var dateModel = new FilterDateModel - { - After = DateTime.UtcNow.AddDays(-1), - Before = DateTime.UtcNow - }; - - var action = new FilterPaneAction.SetFilterDateRangeSuccess(dateModel); - - var result = FilterPaneReducers.ReduceSetFilterDateRangeSuccess(state, action); - - Assert.NotNull(result.FilteredDateRange); - Assert.Equal(dateModel, result.FilteredDateRange); - } - - [Fact] - public void ReduceSetFilterDateRangeSuccess_WithNull_ShouldSetNullDateRange() - { - var state = new FilterPaneState - { - FilteredDateRange = new FilterDateModel { After = DateTime.UtcNow } - }; - - var action = new FilterPaneAction.SetFilterDateRangeSuccess(null); - - var result = FilterPaneReducers.ReduceSetFilterDateRangeSuccess(state, action); - - Assert.Null(result.FilteredDateRange); - } - [Fact] public void ReduceSetIsLoading_ShouldSetValue() { var state = new FilterPaneState { IsLoading = false }; - var result = FilterPaneReducers.ReduceSetIsLoading(state, new FilterPaneAction.SetIsLoading(true)); + var result = Reducers.ReduceSetIsLoading(state, new SetIsLoadingAction(true)); Assert.True(result.IsLoading); } @@ -494,7 +494,7 @@ public void ReduceSetIsLoading_WhenValueUnchanged_ShouldReturnSameState() { var state = new FilterPaneState { IsLoading = true }; - var result = FilterPaneReducers.ReduceSetIsLoading(state, new FilterPaneAction.SetIsLoading(true)); + var result = Reducers.ReduceSetIsLoading(state, new SetIsLoadingAction(true)); Assert.Same(state, result); } @@ -507,7 +507,7 @@ public void ReduceToggleFilterDate_WithDateRange_ShouldToggleIsEnabled() FilteredDateRange = new FilterDateModel { IsEnabled = false } }; - var result = FilterPaneReducers.ReduceToggleFilterDate(state); + var result = Reducers.ReduceToggleFilterDate(state); Assert.NotNull(result.FilteredDateRange); Assert.True(result.FilteredDateRange.IsEnabled); @@ -518,7 +518,7 @@ public void ReduceToggleFilterDate_WithNullDateRange_ShouldReturnOriginalState() { var state = new FilterPaneState { FilteredDateRange = null }; - var result = FilterPaneReducers.ReduceToggleFilterDate(state); + var result = Reducers.ReduceToggleFilterDate(state); Assert.Equal(state, result); } @@ -528,9 +528,9 @@ public void ReduceToggleFilterEnabled_ShouldToggleIsEnabled() { var filter = FilterUtils.CreateTestFilter(isEnabled: false); var state = new FilterPaneState { Filters = [filter] }; - var action = new FilterPaneAction.ToggleFilterEnabled(filter.Id); + var action = new ToggleFilterEnabledAction(filter.Id); - var result = FilterPaneReducers.ReduceToggleFilterEnabled(state, action); + var result = Reducers.ReduceToggleFilterEnabled(state, action); Assert.True(result.Filters[0].IsEnabled); } @@ -540,9 +540,9 @@ public void ReduceToggleFilterEnabled_WithInvalidId_ShouldNotModifyFilters() { var filter = FilterUtils.CreateTestFilter(isEnabled: true); var state = new FilterPaneState { Filters = [filter] }; - var action = new FilterPaneAction.ToggleFilterEnabled(FilterId.Create()); + var action = new ToggleFilterEnabledAction(FilterId.Create()); - var result = FilterPaneReducers.ReduceToggleFilterEnabled(state, action); + var result = Reducers.ReduceToggleFilterEnabled(state, action); Assert.True(result.Filters[0].IsEnabled); } @@ -552,9 +552,9 @@ public void ReduceToggleFilterExcluded_ShouldToggleIsExcluded() { var filter = FilterUtils.CreateTestFilter(isExcluded: false); var state = new FilterPaneState { Filters = [filter] }; - var action = new FilterPaneAction.ToggleFilterExcluded(filter.Id); + var action = new ToggleFilterExcludedAction(filter.Id); - var result = FilterPaneReducers.ReduceToggleFilterExcluded(state, action); + var result = Reducers.ReduceToggleFilterExcluded(state, action); Assert.True(result.Filters[0].IsExcluded); } @@ -564,9 +564,9 @@ public void ReduceToggleFilterExcluded_WithInvalidId_ShouldNotModifyFilters() { var filter = FilterUtils.CreateTestFilter(isExcluded: false); var state = new FilterPaneState { Filters = [filter] }; - var action = new FilterPaneAction.ToggleFilterExcluded(FilterId.Create()); + var action = new ToggleFilterExcludedAction(FilterId.Create()); - var result = FilterPaneReducers.ReduceToggleFilterExcluded(state, action); + var result = Reducers.ReduceToggleFilterExcluded(state, action); Assert.False(result.Filters[0].IsExcluded); } @@ -576,7 +576,7 @@ public void ReduceToggleIsEnabled_ShouldToggleValue() { var state = new FilterPaneState { IsEnabled = false }; - var result = FilterPaneReducers.ReduceToggleIsEnabled(state); + var result = Reducers.ReduceToggleIsEnabled(state); Assert.True(result.IsEnabled); } @@ -599,7 +599,7 @@ public void ClearAllFilters_ShouldResetToDefaultExceptIsEnabled() IsLoading = true }; - state = FilterPaneReducers.ReduceClearFilters(state); + state = Reducers.ReduceClearFilters(state); Assert.Empty(state.Filters); Assert.Null(state.FilteredDateRange); @@ -613,22 +613,22 @@ public void CompleteFilterLifecycle_ShouldManageFilterProperly() var state = new FilterPaneState(); var filter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals100); - state = FilterPaneReducers.ReduceAddFilter(state, new FilterPaneAction.AddFilter(filter)); + state = Reducers.ReduceAddFilter(state, new AddFilterAction(filter)); Assert.Single(state.Filters); - state = FilterPaneReducers.ReduceToggleFilterEnabled( + state = Reducers.ReduceToggleFilterEnabled( state, - new FilterPaneAction.ToggleFilterEnabled(filter.Id)); + new ToggleFilterEnabledAction(filter.Id)); Assert.True(state.Filters[0].IsEnabled); - state = FilterPaneReducers.ReduceToggleFilterExcluded( + state = Reducers.ReduceToggleFilterExcluded( state, - new FilterPaneAction.ToggleFilterExcluded(filter.Id)); + new ToggleFilterExcludedAction(filter.Id)); Assert.True(state.Filters[0].IsExcluded); - state = FilterPaneReducers.ReduceRemoveFilter(state, new FilterPaneAction.RemoveFilter(filter.Id)); + state = Reducers.ReduceRemoveFilter(state, new RemoveFilterAction(filter.Id)); Assert.Empty(state.Filters); } @@ -644,20 +644,20 @@ public void DateRangeFiltering_ShouldManageDateRange() IsEnabled = true }; - state = FilterPaneReducers.ReduceSetFilterDateRangeSuccess( + state = Reducers.ReduceSetFilterDateRangeSuccess( state, - new FilterPaneAction.SetFilterDateRangeSuccess(dateModel)); + new SetFilterDateRangeSuccessAction(dateModel)); Assert.NotNull(state.FilteredDateRange); Assert.True(state.FilteredDateRange.IsEnabled); - state = FilterPaneReducers.ReduceToggleFilterDate(state); + state = Reducers.ReduceToggleFilterDate(state); Assert.NotNull(state.FilteredDateRange); Assert.False(state.FilteredDateRange.IsEnabled); - state = FilterPaneReducers.ReduceSetFilterDateRangeSuccess( + state = Reducers.ReduceSetFilterDateRangeSuccess( state, - new FilterPaneAction.SetFilterDateRangeSuccess(null)); + new SetFilterDateRangeSuccessAction(null)); Assert.Null(state.FilteredDateRange); } @@ -678,9 +678,9 @@ public void FilterGroupApplication_ShouldAddMultipleFilters() ] }; - state = FilterPaneReducers.ReduceApplyFilterGroup( + state = Reducers.ReduceApplyFilterGroup( state, - new FilterPaneAction.ApplyFilterGroup(filterGroup)); + new ApplyFilterGroupAction(filterGroup)); Assert.Equal(3, state.Filters.Count); Assert.All(state.Filters, filter => Assert.True(filter.IsEnabled)); @@ -694,7 +694,7 @@ public void ImmutableCollections_ShouldPreserveImmutability() var state = new FilterPaneState { Filters = originalFilters }; var newFilter = FilterUtils.CreateTestFilter(Constants.FilterIdEquals200); - var newState = FilterPaneReducers.ReduceAddFilter(state, new FilterPaneAction.AddFilter(newFilter)); + var newState = Reducers.ReduceAddFilter(state, new AddFilterAction(newFilter)); Assert.Single(state.Filters); Assert.Equal(2, newState.Filters.Count); @@ -711,13 +711,13 @@ public void MultipleFilters_ShouldMaintainIndependentStates() var filter3 = FilterUtils.CreateTestFilter(Constants.FilterLevelEqualsError); - state = FilterPaneReducers.ReduceAddFilter(state, new FilterPaneAction.AddFilter(filter1)); - state = FilterPaneReducers.ReduceAddFilter(state, new FilterPaneAction.AddFilter(filter2)); - state = FilterPaneReducers.ReduceAddFilter(state, new FilterPaneAction.AddFilter(filter3)); + state = Reducers.ReduceAddFilter(state, new AddFilterAction(filter1)); + state = Reducers.ReduceAddFilter(state, new AddFilterAction(filter2)); + state = Reducers.ReduceAddFilter(state, new AddFilterAction(filter3)); - state = FilterPaneReducers.ReduceToggleFilterEnabled( + state = Reducers.ReduceToggleFilterEnabled( state, - new FilterPaneAction.ToggleFilterEnabled(filter2.Id)); + new ToggleFilterEnabledAction(filter2.Id)); Assert.False(state.Filters[0].IsEnabled); Assert.True(state.Filters[1].IsEnabled); @@ -735,7 +735,7 @@ public void SetFilter_ShouldReplaceExistingFilterWithSameId() isEnabled: false, id: filter.Id); - state = FilterPaneReducers.ReduceSetFilter(state, new FilterPaneAction.SetFilter(updatedFilter)); + state = Reducers.ReduceSetFilter(state, new SetFilterAction(updatedFilter)); Assert.Single(state.Filters); Assert.Equal(Constants.FilterIdEquals200, state.Filters[0].ComparisonText); @@ -751,11 +751,11 @@ public void ToggleOperations_ShouldAllWorkIndependently() IsLoading = false }; - state = FilterPaneReducers.ReduceToggleIsEnabled(state); + state = Reducers.ReduceToggleIsEnabled(state); Assert.True(state.IsEnabled); Assert.False(state.IsLoading); - state = FilterPaneReducers.ReduceSetIsLoading(state, new FilterPaneAction.SetIsLoading(true)); + state = Reducers.ReduceSetIsLoading(state, new SetIsLoadingAction(true)); Assert.True(state.IsEnabled); Assert.True(state.IsLoading); } diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs index f3e69234..9879b1a5 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs @@ -31,7 +31,7 @@ public sealed class StatusBarActionTests public void ClearStatusAction_ShouldCreateAction() { var activityId = Guid.NewGuid(); - var action = new StatusBarAction.ClearStatus(activityId); + var action = new ClearStatusAction(activityId); Assert.Equal(activityId, action.ActivityId); } @@ -39,7 +39,7 @@ public void ClearStatusAction_ShouldCreateAction() [Fact] public void CloseAllAction_ShouldCreateAction() { - var action = new StatusBarAction.CloseAll(); + var action = new CloseAllAction(); Assert.NotNull(action); } @@ -50,7 +50,7 @@ public void SetEventsLoadingAction_ShouldCreateAction() var activityId = Guid.NewGuid(); var count = 100; var failedCount = 5; - var action = new StatusBarAction.SetEventsLoading(activityId, count, failedCount); + var action = new SetEventsLoadingAction(activityId, count, failedCount); Assert.Equal(activityId, action.ActivityId); Assert.Equal(count, action.Count); @@ -61,7 +61,7 @@ public void SetEventsLoadingAction_ShouldCreateAction() public void SetResolverStatusAction_ShouldCreateAction() { var status = "Resolving events..."; - var action = new StatusBarAction.SetResolverStatus(status); + var action = new SetResolverStatusAction(status); Assert.Equal(status, action.ResolverStatus); } @@ -73,9 +73,9 @@ public sealed class StatusBarReducerTests public void ReduceClearStatus_WithEmptyState_ShouldReturnEmptyState() { var state = new StatusBarState(); - var action = new StatusBarAction.ClearStatus(Guid.NewGuid()); + var action = new ClearStatusAction(Guid.NewGuid()); - var result = StatusBarReducers.ReduceClearStatus(state, action); + var result = Reducers.ReduceClearStatus(state, action); Assert.Empty(result.EventsLoading); } @@ -90,9 +90,9 @@ public void ReduceClearStatus_WithExistingActivity_ShouldRemoveActivity() EventsLoading = ImmutableDictionary.Empty.Add(activityId, (100, 5)) }; - var action = new StatusBarAction.ClearStatus(activityId); + var action = new ClearStatusAction(activityId); - var result = StatusBarReducers.ReduceClearStatus(state, action); + var result = Reducers.ReduceClearStatus(state, action); Assert.Empty(result.EventsLoading); } @@ -108,9 +108,9 @@ public void ReduceClearStatus_WithNonExistingActivity_ShouldReturnStateWithoutCh EventsLoading = ImmutableDictionary.Empty.Add(existingActivityId, (100, 5)) }; - var action = new StatusBarAction.ClearStatus(nonExistingActivityId); + var action = new ClearStatusAction(nonExistingActivityId); - var result = StatusBarReducers.ReduceClearStatus(state, action); + var result = Reducers.ReduceClearStatus(state, action); Assert.Single(result.EventsLoading); Assert.True(result.EventsLoading.ContainsKey(existingActivityId)); @@ -127,7 +127,7 @@ public void ReduceCloseAll_ShouldReturnNewDefaultState() ResolverStatus = "Processing..." }; - var result = StatusBarReducers.ReduceCloseAll(state); + var result = Reducers.ReduceCloseAll(state); Assert.Empty(result.EventsLoading); Assert.Equal(string.Empty, result.ResolverStatus); @@ -143,9 +143,9 @@ public void ReduceSetEventsLoading_WithExistingActivity_ShouldUpdateActivity() EventsLoading = ImmutableDictionary.Empty.Add(activityId, (50, 2)) }; - var action = new StatusBarAction.SetEventsLoading(activityId, 100, 5); + var action = new SetEventsLoadingAction(activityId, 100, 5); - var result = StatusBarReducers.ReduceSetEventsLoading(state, action); + var result = Reducers.ReduceSetEventsLoading(state, action); Assert.Single(result.EventsLoading); Assert.Equal((100, 5), result.EventsLoading[activityId]); @@ -165,9 +165,9 @@ public void ReduceSetEventsLoading_WithMultipleActivities_ShouldMaintainOthers() .Add(activityId2, (200, 10)) }; - var action = new StatusBarAction.SetEventsLoading(activityId3, 300, 15); + var action = new SetEventsLoadingAction(activityId3, 300, 15); - var result = StatusBarReducers.ReduceSetEventsLoading(state, action); + var result = Reducers.ReduceSetEventsLoading(state, action); Assert.Equal(3, result.EventsLoading.Count); Assert.True(result.EventsLoading.ContainsKey(activityId1)); @@ -180,9 +180,9 @@ public void ReduceSetEventsLoading_WithNewActivity_ShouldAddActivity() { var state = new StatusBarState(); var activityId = Guid.NewGuid(); - var action = new StatusBarAction.SetEventsLoading(activityId, 100, 5); + var action = new SetEventsLoadingAction(activityId, 100, 5); - var result = StatusBarReducers.ReduceSetEventsLoading(state, action); + var result = Reducers.ReduceSetEventsLoading(state, action); Assert.Single(result.EventsLoading); Assert.True(result.EventsLoading.ContainsKey(activityId)); @@ -199,9 +199,9 @@ public void ReduceSetEventsLoading_WithUnchangedValues_ShouldReturnSameState() EventsLoading = ImmutableDictionary.Empty.Add(activityId, (100, 5)) }; - var action = new StatusBarAction.SetEventsLoading(activityId, 100, 5); + var action = new SetEventsLoadingAction(activityId, 100, 5); - var result = StatusBarReducers.ReduceSetEventsLoading(state, action); + var result = Reducers.ReduceSetEventsLoading(state, action); Assert.Same(state, result); } @@ -216,9 +216,9 @@ public void ReduceSetEventsLoading_WithZeroCount_ShouldRemoveActivity() EventsLoading = ImmutableDictionary.Empty.Add(activityId, (100, 5)) }; - var action = new StatusBarAction.SetEventsLoading(activityId, 0, 0); + var action = new SetEventsLoadingAction(activityId, 0, 0); - var result = StatusBarReducers.ReduceSetEventsLoading(state, action); + var result = Reducers.ReduceSetEventsLoading(state, action); Assert.Empty(result.EventsLoading); } @@ -228,9 +228,9 @@ public void ReduceSetResolverStatus_ShouldReplaceExistingStatus() { var state = new StatusBarState { ResolverStatus = "Old status" }; var newStatus = "New status"; - var action = new StatusBarAction.SetResolverStatus(newStatus); + var action = new SetResolverStatusAction(newStatus); - var result = StatusBarReducers.ReduceSetResolverStatus(state, action); + var result = Reducers.ReduceSetResolverStatus(state, action); Assert.Equal(newStatus, result.ResolverStatus); } @@ -240,9 +240,9 @@ public void ReduceSetResolverStatus_ShouldSetStatus() { var state = new StatusBarState(); var status = "Resolving 50 events..."; - var action = new StatusBarAction.SetResolverStatus(status); + var action = new SetResolverStatusAction(status); - var result = StatusBarReducers.ReduceSetResolverStatus(state, action); + var result = Reducers.ReduceSetResolverStatus(state, action); Assert.Equal(status, result.ResolverStatus); } @@ -251,9 +251,9 @@ public void ReduceSetResolverStatus_ShouldSetStatus() public void ReduceSetResolverStatus_WithEmptyString_ShouldClearStatus() { var state = new StatusBarState { ResolverStatus = "Processing..." }; - var action = new StatusBarAction.SetResolverStatus(string.Empty); + var action = new SetResolverStatusAction(string.Empty); - var result = StatusBarReducers.ReduceSetResolverStatus(state, action); + var result = Reducers.ReduceSetResolverStatus(state, action); Assert.Equal(string.Empty, result.ResolverStatus); } @@ -267,30 +267,30 @@ public void ActivityLifecycle_ShouldHandleAddUpdateRemove() var state = new StatusBarState(); var activityId = Guid.NewGuid(); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activityId, 100, 0)); + new SetEventsLoadingAction(activityId, 100, 0)); Assert.Single(state.EventsLoading); Assert.Equal((100, 0), state.EventsLoading[activityId]); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activityId, 75, 2)); + new SetEventsLoadingAction(activityId, 75, 2)); Assert.Single(state.EventsLoading); Assert.Equal((75, 2), state.EventsLoading[activityId]); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activityId, 50, 5)); + new SetEventsLoadingAction(activityId, 50, 5)); Assert.Single(state.EventsLoading); Assert.Equal((50, 5), state.EventsLoading[activityId]); - state = StatusBarReducers.ReduceClearStatus( + state = Reducers.ReduceClearStatus( state, - new StatusBarAction.ClearStatus(activityId)); + new ClearStatusAction(activityId)); Assert.Empty(state.EventsLoading); } @@ -309,7 +309,7 @@ public void ClearNonExistingActivities_ShouldNotAffectOthers() .Add(activity2, (200, 10)) }; - state = StatusBarReducers.ReduceClearStatus(state, new StatusBarAction.ClearStatus(nonExisting)); + state = Reducers.ReduceClearStatus(state, new ClearStatusAction(nonExisting)); Assert.Equal(2, state.EventsLoading.Count); Assert.True(state.EventsLoading.ContainsKey(activity1)); @@ -328,7 +328,7 @@ public void CloseAll_ShouldResetAllState() ResolverStatus = "Processing multiple activities..." }; - state = StatusBarReducers.ReduceCloseAll(state); + state = Reducers.ReduceCloseAll(state); Assert.Empty(state.EventsLoading); Assert.Equal(string.Empty, state.ResolverStatus); @@ -340,22 +340,22 @@ public void CompleteWorkflow_ShouldHandleLoadingAndResolution() var state = new StatusBarState(); var activityId = Guid.NewGuid(); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activityId, 100, 0)); + new SetEventsLoadingAction(activityId, 100, 0)); Assert.Single(state.EventsLoading); - state = StatusBarReducers.ReduceSetResolverStatus( + state = Reducers.ReduceSetResolverStatus( state, - new StatusBarAction.SetResolverStatus("Resolving events...")); + new SetResolverStatusAction("Resolving events...")); Assert.Equal("Resolving events...", state.ResolverStatus); Assert.Empty(state.EventsLoading); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activityId, 0, 0)); + new SetEventsLoadingAction(activityId, 0, 0)); Assert.Empty(state.EventsLoading); Assert.Equal("Resolving events...", state.ResolverStatus); @@ -368,26 +368,26 @@ public void LoadingProgress_ShouldTrackMultipleActivities() var activity1 = Guid.NewGuid(); var activity2 = Guid.NewGuid(); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activity1, 100, 0)); + new SetEventsLoadingAction(activity1, 100, 0)); Assert.Single(state.EventsLoading); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activity2, 200, 5)); + new SetEventsLoadingAction(activity2, 200, 5)); Assert.Equal(2, state.EventsLoading.Count); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activity1, 50, 0)); + new SetEventsLoadingAction(activity1, 50, 0)); Assert.Equal(2, state.EventsLoading.Count); Assert.Equal((50, 0), state.EventsLoading[activity1]); - state = StatusBarReducers.ReduceClearStatus(state, new StatusBarAction.ClearStatus(activity1)); + state = Reducers.ReduceClearStatus(state, new ClearStatusAction(activity1)); Assert.Single(state.EventsLoading); Assert.True(state.EventsLoading.ContainsKey(activity2)); } @@ -400,17 +400,17 @@ public void MultipleActivitiesWithFailures_ShouldTrackIndependently() var activity2 = Guid.NewGuid(); var activity3 = Guid.NewGuid(); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activity1, 100, 0)); + new SetEventsLoadingAction(activity1, 100, 0)); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activity2, 200, 10)); + new SetEventsLoadingAction(activity2, 200, 10)); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activity3, 150, 5)); + new SetEventsLoadingAction(activity3, 150, 5)); Assert.Equal(3, state.EventsLoading.Count); Assert.Equal((100, 0), state.EventsLoading[activity1]); @@ -424,23 +424,23 @@ public void ResolverStatus_ShouldUpdateIndependentlyOfLoading() var state = new StatusBarState(); var activityId = Guid.NewGuid(); - state = StatusBarReducers.ReduceSetResolverStatus( + state = Reducers.ReduceSetResolverStatus( state, - new StatusBarAction.SetResolverStatus("Starting resolution...")); + new SetResolverStatusAction("Starting resolution...")); Assert.Equal("Starting resolution...", state.ResolverStatus); Assert.Empty(state.EventsLoading); - state = StatusBarReducers.ReduceSetEventsLoading( + state = Reducers.ReduceSetEventsLoading( state, - new StatusBarAction.SetEventsLoading(activityId, 100, 0)); + new SetEventsLoadingAction(activityId, 100, 0)); Assert.Equal("Starting resolution...", state.ResolverStatus); Assert.Single(state.EventsLoading); - state = StatusBarReducers.ReduceSetResolverStatus( + state = Reducers.ReduceSetResolverStatus( state, - new StatusBarAction.SetResolverStatus("Resolution in progress...")); + new SetResolverStatusAction("Resolution in progress...")); Assert.Equal("Resolution in progress...", state.ResolverStatus); Assert.Empty(state.EventsLoading); diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.AppTitle.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.AppTitle.cs index fcce6ef2..b5a4f113 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.AppTitle.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.AppTitle.cs @@ -1,4 +1,7 @@ -namespace EventLogExpert.UI.Tests.TestUtils.Constants; +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Tests.TestUtils.Constants; public sealed partial class Constants { diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs index dc0c6d4a..3c7ff8f3 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Database.cs @@ -5,6 +5,7 @@ namespace EventLogExpert.UI.Tests.TestUtils.Constants; public sealed partial class Constants { + public const string AnotherDb = "AnotherDb"; // Database names public const string DatabaseA = "Database A"; public const string DatabaseB = "Database B"; @@ -15,22 +16,16 @@ public sealed partial class Constants public const string DisabledDb1 = "DisabledDb1"; public const string DisabledDb2 = "DisabledDb2"; public const string InitialDisabled = "InitialDisabled"; + public const string Linux1 = "Linux 1"; public const string NewDisabled1 = "NewDisabled1"; public const string NewDisabled2 = "NewDisabled2"; - - // Versioned database names - public const string Windows9 = "Windows 9"; - public const string Windows10 = "Windows 10"; - public const string Windows11 = "Windows 11"; - public const string Linux1 = "Linux 1"; public const string Server1 = "Server 1"; - public const string Server2 = "Server 2"; public const string Server10 = "Server 10"; + public const string Server2 = "Server 2"; public const string Server20 = "Server 20"; // Non-versioned database names public const string SimpleDatabase = "SimpleDatabase"; - public const string AnotherDb = "AnotherDb"; // Test database file names (EnabledDatabaseCollectionProviderTests) public const string TestDb1 = "TestDb1.db"; @@ -41,4 +36,9 @@ public sealed partial class Constants public const string TestDbPath1 = @"C:\Databases\TestDb1.db"; public const string TestDbPath2 = @"C:\Databases\TestDb2.db"; public const string TestDbPath3 = @"C:\Databases\TestDb3.db"; + public const string Windows10 = "Windows 10"; + public const string Windows11 = "Windows 11"; + + // Versioned database names + public const string Windows9 = "Windows 9"; } diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs index e3a89062..333ff094 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.DebugLog.cs @@ -16,7 +16,7 @@ public sealed partial class Constants public const string DebugLogOlderTimestamp = "2026-04-29T07:53:19.0000000-04:00"; public const string DebugLogSecondMessage = "Second message"; public const string DebugLogTestMessage = "Test message"; - public const string DebugLogTestTimestamp = "2026-04-29T07:53:20.9321852-04:00"; public const int DebugLogTestThreadId = 12; + public const string DebugLogTestTimestamp = "2026-04-29T07:53:20.9321852-04:00"; public const string DebugLogThirdMessage = "Third message"; } diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Deployment.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Deployment.cs index 4aa769be..ec588394 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Deployment.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Deployment.cs @@ -8,12 +8,12 @@ public sealed partial class Constants public const string DownloadPath = @"C:\Downloads\update.msix"; public const string DownloadPathUri = "file:///C:/Downloads/update.msix"; public const string InvalidDownloadPath = "invalid://path"; + public const string ProgressString100 = "Installing: 100%"; public const string ProgressString25 = "Installing: 25%"; public const string ProgressString50 = "Installing: 50%"; - public const string ProgressString100 = "Installing: 100%"; public const string RelaunchMessage = "Relaunch to Apply Update"; + public const string UpdateFailureOk = "Ok"; public const string UpdateFailureTitle = "Update Failure"; - public const string UpdateFailureOk = "Ok"; } diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.EventLog.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.EventLog.cs index 546f9622..d063f94f 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.EventLog.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.EventLog.cs @@ -5,16 +5,15 @@ namespace EventLogExpert.UI.Tests.TestUtils.Constants; public sealed partial class Constants { + // File paths + public const string FilePathTestEvtx = @"C:\Logs\test.evtx"; // Log names public const string LogNameApplication = "Application"; - public const string LogNameTestLog = "TestLog"; public const string LogNameLog1 = "Log1"; public const string LogNameLog2 = "Log2"; public const string LogNameLog3 = "Log3"; public const string LogNameNewLog = "NewLog"; - - // File paths - public const string FilePathTestEvtx = @"C:\Logs\test.evtx"; + public const string LogNameTestLog = "TestLog"; // Max events public const int MaxNewEvents = 1000; diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Filter.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Filter.cs index 102e3159..5b51b5da 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Filter.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Filter.cs @@ -5,47 +5,47 @@ namespace EventLogExpert.UI.Tests.TestUtils.Constants; public sealed partial class Constants { + public const string EventComputerServer01 = "SERVER01"; + public const string EventComputerServer02 = "SERVER02"; + public const string EventDescriptionErrorOccurred = "An error occurred in the application"; + public const string EventDescriptionSuccess = "Operation completed successfully"; + + // Event property values for testing + public const string EventLevelError = "Error"; + public const string EventLevelInformation = "Information"; + public const string EventSourceOtherSource = "OtherSource"; + public const string EventSourceTestSource = "TestSource"; + public const string EventTaskCategorySecurity = "Security Audit"; + public const string EventTaskCategorySystem = "System"; + public const string FilterComputerNameEqualsServer01 = "ComputerName == \"SERVER01\""; + public const string FilterDescriptionContainsErrorOccurred = "Description.Contains(\"error occurred\")"; + public const string FilterGroupDisplayName = "TestGroup"; + public const string FilterGroupName = "TestSection\\TestGroup"; + public const string FilterGroupNameNested = "TestSection\\SubSection\\TestGroup"; + + // Filter group names + public const string FilterGroupSection = "TestSection"; + public const string FilterGroupSubSection = "SubSection"; // Filter expression strings public const string FilterIdEquals100 = "Id == 100"; + public const string FilterIdEquals100AndLevelError = "Id == 100 && Level == \"Error\""; + public const string FilterIdEquals100Or200 = "Id == 100 || Id == 200"; public const string FilterIdEquals200 = "Id == 200"; public const string FilterIdEquals999 = "Id == 999"; - public const string FilterIdNotEquals100 = "Id != 100"; public const string FilterIdGreaterThan100 = "Id > 100"; - public const string FilterIdEquals100Or200 = "Id == 100 || Id == 200"; - public const string FilterIdEquals100AndLevelError = "Id == 100 && Level == \"Error\""; + public const string FilterIdNotEquals100 = "Id != 100"; + public const string FilterInvalidProperty = "InvalidProperty == 100"; + public const string FilterInvalidValue = "Id == invalid"; public const string FilterLevelEqualsError = "Level == \"Error\""; public const string FilterSourceContainsTest = "Source.Contains(\"Test\")"; - public const string FilterDescriptionContainsErrorOccurred = "Description.Contains(\"error occurred\")"; - public const string FilterComputerNameEqualsServer01 = "ComputerName == \"SERVER01\""; public const string FilterTaskCategoryContainsSecurity = "TaskCategory.Contains(\"Security\")"; - public const string FilterInvalidProperty = "InvalidProperty == 100"; - public const string FilterInvalidValue = "Id == invalid"; - - // Event property values for testing - public const string EventLevelError = "Error"; - public const string EventLevelInformation = "Information"; - public const string EventSourceTestSource = "TestSource"; - public const string EventSourceOtherSource = "OtherSource"; - public const string EventDescriptionErrorOccurred = "An error occurred in the application"; - public const string EventDescriptionSuccess = "Operation completed successfully"; - public const string EventComputerServer01 = "SERVER01"; - public const string EventComputerServer02 = "SERVER02"; - public const string EventTaskCategorySecurity = "Security Audit"; - public const string EventTaskCategorySystem = "System"; // Filter data test values public const string FilterValue100 = "100"; + public const string FilterValue1000 = "1000"; public const string FilterValue200 = "200"; public const string FilterValue300 = "300"; public const string FilterValue500 = "500"; - public const string FilterValue1000 = "1000"; - - // Filter group names - public const string FilterGroupSection = "TestSection"; - public const string FilterGroupSubSection = "SubSection"; - public const string FilterGroupName = "TestSection\\TestGroup"; - public const string FilterGroupNameNested = "TestSection\\SubSection\\TestGroup"; - public const string FilterGroupDisplayName = "TestGroup"; // XML filter expressions public const string FilterXmlContainsData = "xml.Contains(\"data\")"; diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.GitHubRelease.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.GitHubRelease.cs index ef97e664..c3169128 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.GitHubRelease.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.GitHubRelease.cs @@ -1,4 +1,7 @@ -namespace EventLogExpert.UI.Tests.TestUtils.Constants; +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.UI.Tests.TestUtils.Constants; public sealed partial class Constants { diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Settings.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Settings.cs index ea56f612..36f52604 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Settings.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/Constants/Constants.Settings.cs @@ -5,8 +5,8 @@ namespace EventLogExpert.UI.Tests.TestUtils.Constants; public sealed partial class Constants { + public const string TimeZoneEastern = "Eastern Standard Time"; + public const string TimeZonePacific = "Pacific Standard Time"; // TimeZone IDs public const string TimeZoneUtc = "UTC"; - public const string TimeZonePacific = "Pacific Standard Time"; - public const string TimeZoneEastern = "Eastern Standard Time"; } diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs index 87ba359a..6436aa15 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs @@ -1,4 +1,7 @@ -using EventLogExpert.UI.Models; +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.UI.Models; using static EventLogExpert.UI.Tests.TestUtils.Constants.Constants; namespace EventLogExpert.UI.Tests.TestUtils; @@ -7,7 +10,7 @@ public static class GitHubUtils { public static IEnumerable CreateGitReleaseModels() => [ - new GitReleaseModel + new() { Version = GitHubPrereleaseVersion, IsPreRelease = true, @@ -22,7 +25,7 @@ public static IEnumerable CreateGitReleaseModels() => ], RawChanges = GitHubReleaseNotes }, - new GitReleaseModel + new() { Version = GitHubLatestVersion, IsPreRelease = false, From a2a7c61fddcf7fd008eae2472befc1b394dd7b81 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 17:27:36 -0500 Subject: [PATCH 02/33] Move Clipboard port to Common, rename CopyType to EventCopyFormat --- .../BannerHost.razor.cs | 1 + .../Menu/MenuBar.razor.cs | 24 +++--- .../Modals/DebugLogModal.razor.cs | 1 + .../Modals/Filters/FilterGroup.razor.cs | 1 + .../Modals/SettingsModal.razor | 7 +- .../Modals/SettingsModal.razor.cs | 7 +- .../Clipboard/EventCopyFormat.cs} | 4 +- .../Clipboard}/IClipboardService.cs | 4 +- .../Interfaces/IMenuActionService.cs | 4 +- .../Interfaces/IPreferencesProvider.cs | 3 +- .../Services/Settings/ISettingsService.cs | 5 +- .../Services/Settings/SettingsService.cs | 19 +++-- .../Components/Sections/EventTable.razor.cs | 9 +- src/EventLogExpert/MauiProgram.cs | 1 + .../Services/ClipboardService.cs | 35 ++++---- .../Services/KeyboardShortcutService.cs | 2 +- .../Services/MauiMenuActionService.cs | 3 +- .../Services/PreferencesProvider.cs | 13 +-- .../BannerHostTests.cs | 1 + .../Modals/DebugLogModalTests.cs | 1 + .../Services/Settings/SettingsServiceTests.cs | 85 +++++++++++-------- 21 files changed, 131 insertions(+), 99 deletions(-) rename src/EventLogExpert.UI/{Enums/CopyType.cs => Common/Clipboard/EventCopyFormat.cs} (63%) rename src/EventLogExpert.UI/{Interfaces => Common/Clipboard}/IClipboardService.cs (86%) diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index 6a877e43..c88261ab 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index 670c52b0..3de35c7f 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Common.Channels; -using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; @@ -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/Modals/DebugLogModal.razor.cs b/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs index 754c8879..5cb507c4 100644 --- a/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs index 72af5cc0..143ac9aa 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; 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 2df9b9fc..5a634e25 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; @@ -21,7 +22,7 @@ public sealed partial class SettingsModal : ModalBase 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; @@ -104,7 +105,7 @@ protected override async Task OnSaveAsync() await ApplyPendingToggles(); - Settings.CopyType = _copyType; + Settings.CopyFormat = _copyFormat; Settings.IsPreReleaseEnabled = _isPreReleaseEnabled; Settings.LogLevel = _logLevel; Settings.ShowDisplayPaneOnSelectionChange = _showDisplayPaneOnSelectionChange; @@ -225,7 +226,7 @@ await AlertDialogService.ShowAlert("Import Failed", private void LoadFromSettings() { - _copyType = Settings.CopyType; + _copyFormat = Settings.CopyFormat; _isPreReleaseEnabled = Settings.IsPreReleaseEnabled; _logLevel = Settings.LogLevel; _showDisplayPaneOnSelectionChange = Settings.ShowDisplayPaneOnSelectionChange; diff --git a/src/EventLogExpert.UI/Enums/CopyType.cs b/src/EventLogExpert.UI/Common/Clipboard/EventCopyFormat.cs similarity index 63% rename from src/EventLogExpert.UI/Enums/CopyType.cs rename to src/EventLogExpert.UI/Common/Clipboard/EventCopyFormat.cs index 455931cb..3c35abd5 100644 --- a/src/EventLogExpert.UI/Enums/CopyType.cs +++ b/src/EventLogExpert.UI/Common/Clipboard/EventCopyFormat.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Common.Clipboard; -public enum CopyType +public enum EventCopyFormat { Default, Simple, diff --git a/src/EventLogExpert.UI/Interfaces/IClipboardService.cs b/src/EventLogExpert.UI/Common/Clipboard/IClipboardService.cs similarity index 86% rename from src/EventLogExpert.UI/Interfaces/IClipboardService.cs rename to src/EventLogExpert.UI/Common/Clipboard/IClipboardService.cs index c9d4fa3b..b5bc9bd6 100644 --- a/src/EventLogExpert.UI/Interfaces/IClipboardService.cs +++ b/src/EventLogExpert.UI/Common/Clipboard/IClipboardService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Clipboard; public interface IClipboardService { @@ -10,7 +10,7 @@ public interface IClipboardService /// to the clipboard. Implementations must not throw; any failure is logged internally so callers can invoke without /// try/catch. /// - Task CopySelectedEvent(CopyType? copyType = null); + Task CopySelectedEvent(EventCopyFormat? format = null); /// /// Best-effort copy of the supplied text to the clipboard. Implementations must not throw; any failure is logged diff --git a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs index 85edf897..24a39840 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs +++ b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs @@ -1,6 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Clipboard; + namespace EventLogExpert.UI.Interfaces; public interface IMenuActionService @@ -11,7 +13,7 @@ public interface IMenuActionService Task CloseAllLogsAsync(); - Task CopySelectedAsync(CopyType? copyType); + Task CopySelectedAsync(EventCopyFormat? format); void Exit(); diff --git a/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs b/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs index 7d8563c5..1b3b3238 100644 --- a/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs +++ b/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Models; using Microsoft.Extensions.Logging; @@ -22,7 +23,7 @@ public interface IPreferencesProvider IEnumerable FavoriteFiltersPreference { get; set; } - CopyType KeyboardCopyTypePreference { get; set; } + EventCopyFormat KeyboardCopyFormatPreference { get; set; } LogLevel LogLevelPreference { get; set; } diff --git a/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs b/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs index 7f6c23cb..f699bdfb 100644 --- a/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs +++ b/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs @@ -1,15 +1,16 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Clipboard; using Microsoft.Extensions.Logging; namespace EventLogExpert.UI.Interfaces; 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/Settings/SettingsService.cs b/src/EventLogExpert.UI/Services/Settings/SettingsService.cs index 95e8f097..b6ecb1f7 100644 --- a/src/EventLogExpert.UI/Services/Settings/SettingsService.cs +++ b/src/EventLogExpert.UI/Services/Settings/SettingsService.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using Microsoft.Extensions.Logging; @@ -10,32 +11,32 @@ public sealed class SettingsService(IPreferencesProvider preferences) : ISetting { 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/Components/Sections/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index 9733468d..eec96045 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; @@ -916,10 +917,10 @@ private IReadOnlyList ShowContextMenuItems(ResolvedEvent selectedEvent { return [ - MenuItem.Item("Copy Selected", () => ClipboardService.CopySelectedEvent(CopyType.Default)), - MenuItem.Item("Copy Selected (Simple)", () => ClipboardService.CopySelectedEvent(CopyType.Simple)), - MenuItem.Item("Copy Selected (XML)", () => ClipboardService.CopySelectedEvent(CopyType.Xml)), - MenuItem.Item("Copy Selected (Full)", () => ClipboardService.CopySelectedEvent(CopyType.Full)), + MenuItem.Item("Copy Selected", () => ClipboardService.CopySelectedEvent(EventCopyFormat.Default)), + MenuItem.Item("Copy Selected (Simple)", () => ClipboardService.CopySelectedEvent(EventCopyFormat.Simple)), + MenuItem.Item("Copy Selected (XML)", () => ClipboardService.CopySelectedEvent(EventCopyFormat.Xml)), + MenuItem.Item("Copy Selected (Full)", () => ClipboardService.CopySelectedEvent(EventCopyFormat.Full)), MenuItem.Separator(), MenuItem.Item("Exclude Events Before", () => Dispatcher.Dispatch(new SetFilterDateRangeAction( diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index eb9863b9..3c729e98 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Services; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index 99938905..65e297d8 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -5,6 +5,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; @@ -45,14 +46,14 @@ public ClipboardService( _selectedEvent.Select(s => s.SelectedEvent); } - public async Task CopySelectedEvent(CopyType? copyType = null) + public async Task CopySelectedEvent(EventCopyFormat? format = null) { // Copy is best-effort: most callers are Blazor event handlers that don't catch, so any // failure (clipboard unavailable, XML parse, resolver fault) would surface as an // unhandled UI exception. Log and swallow to preserve previous fire-and-forget behavior. try { - string stringToCopy = await GetFormattedEvent(copyType).ConfigureAwait(false); + string stringToCopy = await GetFormattedEvent(format).ConfigureAwait(false); await Clipboard.SetTextAsync(stringToCopy).ConfigureAwait(false); } @@ -93,13 +94,13 @@ private static string GetLogShortName(string owningLog) => private void AppendFormattedEvent( StringBuilder builder, - CopyType copyType, + EventCopyFormat format, ResolvedEvent @event, string xml) { - switch (copyType) + switch (format) { - case CopyType.Default: + case EventCopyFormat.Default: foreach ((ColumnName column, _) in _eventTableColumns.Value.Where(x => x.Value)) { switch (column) @@ -145,18 +146,18 @@ private void AppendFormattedEvent( builder.Append($"\"{@event.Description}\""); break; - case CopyType.Simple: + case EventCopyFormat.Simple: builder.Append($"\"{@event.Level}\" "); builder.Append($"\"{@event.TimeCreated.ConvertTimeZone(_settings.TimeZoneInfo)}\" "); builder.Append($"\"{@event.Source}\" "); builder.Append($"\"{@event.Id}\" "); builder.Append($"\"{@event.Description}\""); break; - case CopyType.Xml: + case EventCopyFormat.Xml: if (!string.IsNullOrEmpty(xml)) { builder.Append(FormatXmlForCopy(xml)); } break; - case CopyType.Full: + case EventCopyFormat.Full: default: builder.AppendLine($"Log Name: {@event.LogName}"); builder.AppendLine($"Source: {@event.Source}"); @@ -180,21 +181,21 @@ private void AppendFormattedEvent( } } - private string FormatEventForCopy(CopyType copyType, ResolvedEvent @event, string xml) + private string FormatEventForCopy(EventCopyFormat format, ResolvedEvent @event, string xml) { - if (copyType == CopyType.Xml) + if (format == EventCopyFormat.Xml) { return string.IsNullOrEmpty(xml) ? string.Empty : FormatXmlForCopy(xml); } StringBuilder builder = new(); - AppendFormattedEvent(builder, copyType, @event, xml); + AppendFormattedEvent(builder, format, @event, xml); return builder.ToString(); } - private async Task GetFormattedEvent(CopyType? copyType) + private async Task GetFormattedEvent(EventCopyFormat? format) { // Snapshot the selection once. Re-reading _selectedEvents.Value across awaits could see // a different (or empty) list if selection changes mid-resolve, leading to copying the @@ -202,8 +203,8 @@ private async Task GetFormattedEvent(CopyType? copyType) var events = _selectedEvents.Value; var selected = _selectedEvent.Value; - var resolvedType = copyType ?? _settings.CopyType; - bool needsXml = resolvedType is CopyType.Xml or CopyType.Full; + var resolvedFormat = format ?? _settings.CopyFormat; + bool needsXml = resolvedFormat is EventCopyFormat.Xml or EventCopyFormat.Full; // Single-event copy: prefer the selected (focused) row so right-click → copy // targets the focused event. Fall back to the only selected event when selected @@ -215,7 +216,7 @@ private async Task GetFormattedEvent(CopyType? copyType) string xml = needsXml ? await _xmlResolver.GetXmlAsync(selected) : string.Empty; - return FormatEventForCopy(resolvedType, selected, xml); + return FormatEventForCopy(resolvedFormat, selected, xml); } if (events.Count == 1) @@ -228,7 +229,7 @@ private async Task GetFormattedEvent(CopyType? copyType) // copies the focused row in that path. string xml = needsXml ? await _xmlResolver.GetXmlAsync(events[0]) : string.Empty; - return FormatEventForCopy(resolvedType, events[0], xml); + return FormatEventForCopy(resolvedFormat, events[0], xml); } string[] xmlByIndex; @@ -260,7 +261,7 @@ private async Task GetFormattedEvent(CopyType? copyType) { string xml = needsXml ? xmlByIndex[i] : string.Empty; - AppendFormattedEvent(stringToCopy, resolvedType, events[i], xml); + AppendFormattedEvent(stringToCopy, resolvedFormat, events[i], xml); stringToCopy.AppendLine(); } diff --git a/src/EventLogExpert/Services/KeyboardShortcutService.cs b/src/EventLogExpert/Services/KeyboardShortcutService.cs index 78d2718e..8c28e34b 100644 --- a/src/EventLogExpert/Services/KeyboardShortcutService.cs +++ b/src/EventLogExpert/Services/KeyboardShortcutService.cs @@ -86,7 +86,7 @@ public async Task HandleShortcutAsync(string code, bool ctrl, bool alt, bool shi return; case "KeyC": - await _actions.CopySelectedAsync(_settings.CopyType); + await _actions.CopySelectedAsync(_settings.CopyFormat); return; } } diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 6922fdea..941dd2dd 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -8,6 +8,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Platforms.Windows; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; @@ -71,7 +72,7 @@ public async Task CloseAllLogsAsync() _dispatcher.Dispatch(new CloseAllAction()); } - public async Task CopySelectedAsync(CopyType? copyType) => await _clipboardService.CopySelectedEvent(copyType); + public async Task CopySelectedAsync(EventCopyFormat? format) => await _clipboardService.CopySelectedEvent(format); public void Dispose() { diff --git a/src/EventLogExpert/Services/PreferencesProvider.cs b/src/EventLogExpert/Services/PreferencesProvider.cs index d65d9ca0..884da161 100644 --- a/src/EventLogExpert/Services/PreferencesProvider.cs +++ b/src/EventLogExpert/Services/PreferencesProvider.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.Extensions.Logging; @@ -18,7 +19,7 @@ public sealed class PreferencesProvider : IPreferencesProvider private const string DisplaySelectionEnabled = "display-selection-enabled"; private const string EnabledEventTableColumns = "enabled-event-table-columns"; private const string FavoriteFilters = "favorite-filters"; - private const string KeyboardCopyType = "keyboard-copy-type"; + private const string KeyboardCopyFormat = "keyboard-copy-format"; private const string LoggingLevel = "logging-level"; private const string PreReleaseEnabled = "prerelease-enabled"; private const string RecentFilters = "recent-filters"; @@ -73,12 +74,12 @@ public IEnumerable FavoriteFiltersPreference set => Preferences.Default.Set(FavoriteFilters, JsonSerializer.Serialize(value)); } - public CopyType KeyboardCopyTypePreference + public EventCopyFormat KeyboardCopyFormatPreference { - get => Enum.TryParse(Preferences.Default.Get(KeyboardCopyType, nameof(CopyType.Full)), - out CopyType value) ? - value : CopyType.Full; - set => Preferences.Default.Set(KeyboardCopyType, value.ToString()); + get => Enum.TryParse(Preferences.Default.Get(KeyboardCopyFormat, nameof(EventCopyFormat.Full)), + out EventCopyFormat value) ? + value : EventCopyFormat.Full; + set => Preferences.Default.Set(KeyboardCopyFormat, value.ToString()); } public LogLevel LogLevelPreference diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs index 09a59879..bece5f9c 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -4,6 +4,7 @@ using Bunit; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components.Web; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs index a2bcc03f..7450fe13 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs @@ -5,6 +5,7 @@ using EventLogExpert.Components.Modals; using EventLogExpert.Components.Tests.TestUtils; using EventLogExpert.Components.Tests.TestUtils.Constants; +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using Fluxor; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs index c446a1f3..1a53c352 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; @@ -22,41 +23,41 @@ public void Constructor_WhenCalled_ShouldNotThrow() } [Fact] - public void CopyType_WhenAccessedMultipleTimes_ShouldCacheValue() + public void CopyFormat_WhenAccessedMultipleTimes_ShouldCacheValue() { // Arrange var mockPreferences = Substitute.For(); - mockPreferences.KeyboardCopyTypePreference.Returns(CopyType.Full); + mockPreferences.KeyboardCopyFormatPreference.Returns(EventCopyFormat.Full); var settingsService = CreateSettingsService(mockPreferences); // Act - _ = settingsService.CopyType; - _ = settingsService.CopyType; - _ = settingsService.CopyType; + _ = settingsService.CopyFormat; + _ = settingsService.CopyFormat; + _ = settingsService.CopyFormat; // Assert - _ = mockPreferences.Received(1).KeyboardCopyTypePreference; + _ = mockPreferences.Received(1).KeyboardCopyFormatPreference; } [Fact] - public void CopyType_WhenFirstAccessed_ShouldReturnFromPreferences() + public void CopyFormat_WhenFirstAccessed_ShouldReturnFromPreferences() { // Arrange var mockPreferences = Substitute.For(); - mockPreferences.KeyboardCopyTypePreference.Returns(CopyType.Xml); + mockPreferences.KeyboardCopyFormatPreference.Returns(EventCopyFormat.Xml); var settingsService = CreateSettingsService(mockPreferences); // Act - var result = settingsService.CopyType; + var result = settingsService.CopyFormat; // Assert - Assert.Equal(CopyType.Xml, result); + Assert.Equal(EventCopyFormat.Xml, result); } [Fact] - public void CopyType_WhenPreferenceIsNull_ShouldReturnDefault() + public void CopyFormat_WhenPreferenceIsNull_ShouldReturnDefault() { // Arrange var mockPreferences = Substitute.For(); @@ -64,85 +65,99 @@ public void CopyType_WhenPreferenceIsNull_ShouldReturnDefault() var settingsService = CreateSettingsService(mockPreferences); // Act - var result = settingsService.CopyType; + var result = settingsService.CopyFormat; // Assert - Assert.Equal(CopyType.Default, result); + Assert.Equal(EventCopyFormat.Default, result); } [Fact] - public void CopyType_WhenSetToDifferentValue_ShouldInvokeChangedEvent() + public void CopyFormat_WhenSetToDifferentValue_ShouldInvokeChangedEvent() { // Arrange var mockPreferences = Substitute.For(); var settingsService = CreateSettingsService(mockPreferences); var eventInvoked = false; - settingsService.CopyTypeChanged = () => eventInvoked = true; + settingsService.CopyFormatChanged = () => eventInvoked = true; // Act - settingsService.CopyType = CopyType.Simple; + settingsService.CopyFormat = EventCopyFormat.Simple; // Assert Assert.True(eventInvoked); } [Theory] - [InlineData(CopyType.Default)] - [InlineData(CopyType.Simple)] - [InlineData(CopyType.Xml)] - [InlineData(CopyType.Full)] - public void CopyType_WhenSetToEachValue_ShouldPersistCorrectly(CopyType copyType) + [InlineData(EventCopyFormat.Default)] + [InlineData(EventCopyFormat.Simple)] + [InlineData(EventCopyFormat.Xml)] + [InlineData(EventCopyFormat.Full)] + public void CopyFormat_WhenSetToEachValue_ShouldPersistCorrectly(EventCopyFormat copyFormat) { // Arrange var mockPreferences = Substitute.For(); var settingsService = CreateSettingsService(mockPreferences); // Act - settingsService.CopyType = copyType; + settingsService.CopyFormat = copyFormat; // Assert - mockPreferences.Received(1).KeyboardCopyTypePreference = copyType; + mockPreferences.Received(1).KeyboardCopyFormatPreference = copyFormat; } [Fact] - public void CopyType_WhenSetToSameValue_ShouldNotInvokeChangedEvent() + public void CopyFormat_WhenSetToSameValue_ShouldNotInvokeChangedEvent() { // Arrange var mockPreferences = Substitute.For(); - mockPreferences.KeyboardCopyTypePreference.Returns(CopyType.Xml); + mockPreferences.KeyboardCopyFormatPreference.Returns(EventCopyFormat.Xml); var settingsService = CreateSettingsService(mockPreferences); - _ = settingsService.CopyType; // Cache the value + _ = settingsService.CopyFormat; // Cache the value var eventInvoked = false; - settingsService.CopyTypeChanged = () => eventInvoked = true; + settingsService.CopyFormatChanged = () => eventInvoked = true; // Act - settingsService.CopyType = CopyType.Xml; + settingsService.CopyFormat = EventCopyFormat.Xml; // Assert Assert.False(eventInvoked); } [Fact] - public void CopyType_WhenSetToSameValue_ShouldNotUpdatePreferences() + public void CopyFormat_WhenSetToSameValue_ShouldNotUpdatePreferences() { // Arrange var mockPreferences = Substitute.For(); - mockPreferences.KeyboardCopyTypePreference.Returns(CopyType.Xml); + mockPreferences.KeyboardCopyFormatPreference.Returns(EventCopyFormat.Xml); var settingsService = CreateSettingsService(mockPreferences); // First access caches the value - _ = settingsService.CopyType; + _ = settingsService.CopyFormat; mockPreferences.ClearReceivedCalls(); // Act - settingsService.CopyType = CopyType.Xml; + settingsService.CopyFormat = EventCopyFormat.Xml; // Assert - mockPreferences.DidNotReceive().KeyboardCopyTypePreference = Arg.Any(); + mockPreferences.DidNotReceive().KeyboardCopyFormatPreference = Arg.Any(); + } + + [Fact] + public void CopyFormat_WhenSet_ShouldUpdatePreferences() + { + // Arrange + var mockPreferences = Substitute.For(); + var settingsService = CreateSettingsService(mockPreferences); + + // Act + settingsService.CopyFormat = EventCopyFormat.Full; + + // Assert + mockPreferences.Received(1).KeyboardCopyFormatPreference = EventCopyFormat.Full; } [Fact] @@ -153,10 +168,10 @@ public void CopyType_WhenSet_ShouldUpdatePreferences() var settingsService = CreateSettingsService(mockPreferences); // Act - settingsService.CopyType = CopyType.Full; + settingsService.CopyFormat = EventCopyFormat.Full; // Assert - mockPreferences.Received(1).KeyboardCopyTypePreference = CopyType.Full; + mockPreferences.Received(1).KeyboardCopyFormatPreference = EventCopyFormat.Full; } [Fact] From 09f9bb602edcb668e4cadceaff42735c4d6c208d Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 18:04:44 -0500 Subject: [PATCH 03/33] Move IPreferencesProvider port to Common/Preferences/ --- .../Preferences}/IPreferencesProvider.cs | 4 ++-- src/EventLogExpert.UI/Services/Database/DatabaseService.cs | 3 ++- src/EventLogExpert.UI/Services/Settings/SettingsService.cs | 3 ++- src/EventLogExpert.UI/Store/EventTable/Effects.cs | 2 +- src/EventLogExpert.UI/Store/FilterCache/Effects.cs | 2 +- src/EventLogExpert.UI/Store/FilterGroup/Effects.cs | 2 +- src/EventLogExpert/Components/Sections/DetailsPane.razor.cs | 1 + src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/PreferencesProvider.cs | 4 ++-- .../Services/Database/DatabaseServiceTests.cs | 1 + .../Services/Settings/SettingsServiceTests.cs | 2 +- .../EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs | 2 +- .../EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs | 2 +- .../EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs | 2 +- 14 files changed, 18 insertions(+), 13 deletions(-) rename src/EventLogExpert.UI/{Interfaces => Common/Preferences}/IPreferencesProvider.cs (91%) diff --git a/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs b/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs similarity index 91% rename from src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs rename to src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs index 1b3b3238..4e47975f 100644 --- a/src/EventLogExpert.UI/Interfaces/IPreferencesProvider.cs +++ b/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs @@ -1,11 +1,11 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Models; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Common.Preferences; public interface IPreferencesProvider { diff --git a/src/EventLogExpert.UI/Services/Database/DatabaseService.cs b/src/EventLogExpert.UI/Services/Database/DatabaseService.cs index f017cf4b..c257c202 100644 --- a/src/EventLogExpert.UI/Services/Database/DatabaseService.cs +++ b/src/EventLogExpert.UI/Services/Database/DatabaseService.cs @@ -1,9 +1,10 @@ -// // 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.Common.Preferences; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert.UI/Services/Settings/SettingsService.cs b/src/EventLogExpert.UI/Services/Settings/SettingsService.cs index b6ecb1f7..1d14ea69 100644 --- a/src/EventLogExpert.UI/Services/Settings/SettingsService.cs +++ b/src/EventLogExpert.UI/Services/Settings/SettingsService.cs @@ -1,7 +1,8 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Interfaces; using Microsoft.Extensions.Logging; diff --git a/src/EventLogExpert.UI/Store/EventTable/Effects.cs b/src/EventLogExpert.UI/Store/EventTable/Effects.cs index 13407a82..6d02f1d5 100644 --- a/src/EventLogExpert.UI/Store/EventTable/Effects.cs +++ b/src/EventLogExpert.UI/Store/EventTable/Effects.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using Fluxor; using System.Collections.Immutable; diff --git a/src/EventLogExpert.UI/Store/FilterCache/Effects.cs b/src/EventLogExpert.UI/Store/FilterCache/Effects.cs index c749f9ce..a7f1823c 100644 --- a/src/EventLogExpert.UI/Store/FilterCache/Effects.cs +++ b/src/EventLogExpert.UI/Store/FilterCache/Effects.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using Fluxor; using System.Collections.Immutable; diff --git a/src/EventLogExpert.UI/Store/FilterGroup/Effects.cs b/src/EventLogExpert.UI/Store/FilterGroup/Effects.cs index 92a05026..2eb6d394 100644 --- a/src/EventLogExpert.UI/Store/FilterGroup/Effects.cs +++ b/src/EventLogExpert.UI/Store/FilterGroup/Effects.cs @@ -1,7 +1,7 @@ // // 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; diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index 9c3d0147..e8b6c7b3 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.Common.Events; using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Store.EventLog; using Fluxor; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 3c729e98..b579a664 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -8,6 +8,7 @@ using EventLogExpert.Services; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert/Services/PreferencesProvider.cs b/src/EventLogExpert/Services/PreferencesProvider.cs index 884da161..5d3e9014 100644 --- a/src/EventLogExpert/Services/PreferencesProvider.cs +++ b/src/EventLogExpert/Services/PreferencesProvider.cs @@ -1,9 +1,9 @@ -// // Copyright (c) Microsoft Corporation. +// // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. using EventLogExpert.UI; using EventLogExpert.UI.Common.Clipboard; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Models; using Microsoft.Extensions.Logging; using System.Text.Json; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs index 038de850..1d44daad 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Options; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs index 1a53c352..fe3f2692 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs @@ -2,7 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Common.Clipboard; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs index 27fe3f6d..c0771e9a 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventTable/EffectsTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Store.EventTable; using Fluxor; using NSubstitute; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs index e81a4a92..006ca73d 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterCache/EffectsTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Store.FilterCache; using EventLogExpert.UI.Tests.TestUtils.Constants; using Fluxor; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs index a0a293b8..88505c2f 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/FilterGroup/EffectsTests.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterGroup; using EventLogExpert.UI.Tests.TestUtils.Constants; From 79bad808cf5da9b23c91e41f4e99483fd249d937 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 18:17:12 -0500 Subject: [PATCH 04/33] Move Versioning ports to Common/Versioning/ --- src/EventLogExpert.Components/Menu/MenuBar.razor.cs | 2 +- .../{Services => Common}/Versioning/CurrentVersionProvider.cs | 2 +- .../{Services => Common}/Versioning/ICurrentVersionProvider.cs | 2 +- .../{Services => Common}/Versioning/IPackageVersionProvider.cs | 2 +- .../{Services => Common}/Versioning/PackageVersionProvider.cs | 3 +-- src/EventLogExpert.UI/Services/System/AppTitleService.cs | 1 + src/EventLogExpert.UI/Services/Update/UpdateService.cs | 1 + src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/MauiMenuActionService.cs | 1 + .../Services/System/AppTitleServiceTests.cs | 1 + .../Services/Update/UpdateServiceTests.cs | 1 + 11 files changed, 11 insertions(+), 6 deletions(-) rename src/EventLogExpert.UI/{Services => Common}/Versioning/CurrentVersionProvider.cs (94%) rename src/EventLogExpert.UI/{Services => Common}/Versioning/ICurrentVersionProvider.cs (85%) rename src/EventLogExpert.UI/{Services => Common}/Versioning/IPackageVersionProvider.cs (77%) rename src/EventLogExpert.UI/{Services => Common}/Versioning/PackageVersionProvider.cs (85%) diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index 3de35c7f..e41d0ba6 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -3,9 +3,9 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; using Fluxor; diff --git a/src/EventLogExpert.UI/Services/Versioning/CurrentVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs similarity index 94% rename from src/EventLogExpert.UI/Services/Versioning/CurrentVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs index 1764f799..67d00d3e 100644 --- a/src/EventLogExpert.UI/Services/Versioning/CurrentVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Interfaces; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Versioning; public sealed class CurrentVersionProvider( IPackageVersionProvider packageVersionProvider, diff --git a/src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/ICurrentVersionProvider.cs similarity index 85% rename from src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/ICurrentVersionProvider.cs index c4ece453..c37eee79 100644 --- a/src/EventLogExpert.UI/Services/Versioning/ICurrentVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/ICurrentVersionProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Versioning; public interface ICurrentVersionProvider { diff --git a/src/EventLogExpert.UI/Services/Versioning/IPackageVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/IPackageVersionProvider.cs similarity index 77% rename from src/EventLogExpert.UI/Services/Versioning/IPackageVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/IPackageVersionProvider.cs index 00a14fe3..d6f1ed12 100644 --- a/src/EventLogExpert.UI/Services/Versioning/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/Versioning/PackageVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/PackageVersionProvider.cs similarity index 85% rename from src/EventLogExpert.UI/Services/Versioning/PackageVersionProvider.cs rename to src/EventLogExpert.UI/Common/Versioning/PackageVersionProvider.cs index 35f354b2..0c09542a 100644 --- a/src/EventLogExpert.UI/Services/Versioning/PackageVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/PackageVersionProvider.cs @@ -1,10 +1,9 @@ // // 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 { diff --git a/src/EventLogExpert.UI/Services/System/AppTitleService.cs b/src/EventLogExpert.UI/Services/System/AppTitleService.cs index 7c99e0b7..844af065 100644 --- a/src/EventLogExpert.UI/Services/System/AppTitleService.cs +++ b/src/EventLogExpert.UI/Services/System/AppTitleService.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using System.Text; diff --git a/src/EventLogExpert.UI/Services/Update/UpdateService.cs b/src/EventLogExpert.UI/Services/Update/UpdateService.cs index 21c438e9..3e29a732 100644 --- a/src/EventLogExpert.UI/Services/Update/UpdateService.cs +++ b/src/EventLogExpert.UI/Services/Update/UpdateService.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index b579a664..78f48116 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -9,6 +9,7 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Common.Preferences; +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 941dd2dd..2b1dd9ab 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -9,6 +9,7 @@ using EventLogExpert.Platforms.Windows; using EventLogExpert.UI; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs index b8dbc016..3e1f47af 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs index c0088d54..deefae3b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; From 255293a8da433d727e079662c4cb50aa92eaaa6e Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 18:31:25 -0500 Subject: [PATCH 05/33] Move AppTitle ports to Common/AppTitle/ --- .../{Services/System => Common/AppTitle}/AppTitleService.cs | 3 +-- .../{Services/System => Common/AppTitle}/IAppTitleService.cs | 2 +- .../{Services/System => Common/AppTitle}/ITitleProvider.cs | 2 +- src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs | 1 + src/EventLogExpert.UI/Services/Update/UpdateService.cs | 1 + src/EventLogExpert/App.xaml.cs | 1 + src/EventLogExpert/Components/Layout/MainLayout.razor.cs | 1 + src/EventLogExpert/MainPage.xaml.cs | 1 + src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/TitleProvider.cs | 2 +- .../Services/Deployment/DeploymentServiceTests.cs | 1 + .../Services/System/AppTitleServiceTests.cs | 3 +-- .../Services/Update/UpdateServiceTests.cs | 1 + 13 files changed, 13 insertions(+), 7 deletions(-) rename src/EventLogExpert.UI/{Services/System => Common/AppTitle}/AppTitleService.cs (95%) rename src/EventLogExpert.UI/{Services/System => Common/AppTitle}/IAppTitleService.cs (85%) rename src/EventLogExpert.UI/{Services/System => Common/AppTitle}/ITitleProvider.cs (79%) diff --git a/src/EventLogExpert.UI/Services/System/AppTitleService.cs b/src/EventLogExpert.UI/Common/AppTitle/AppTitleService.cs similarity index 95% rename from src/EventLogExpert.UI/Services/System/AppTitleService.cs rename to src/EventLogExpert.UI/Common/AppTitle/AppTitleService.cs index 844af065..d08be335 100644 --- a/src/EventLogExpert.UI/Services/System/AppTitleService.cs +++ b/src/EventLogExpert.UI/Common/AppTitle/AppTitleService.cs @@ -2,10 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Interfaces; using System.Text; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.AppTitle; public sealed class AppTitleService( ICurrentVersionProvider versionProvider, diff --git a/src/EventLogExpert.UI/Services/System/IAppTitleService.cs b/src/EventLogExpert.UI/Common/AppTitle/IAppTitleService.cs similarity index 85% rename from src/EventLogExpert.UI/Services/System/IAppTitleService.cs rename to src/EventLogExpert.UI/Common/AppTitle/IAppTitleService.cs index 3411524a..c01fd6ee 100644 --- a/src/EventLogExpert.UI/Services/System/IAppTitleService.cs +++ b/src/EventLogExpert.UI/Common/AppTitle/IAppTitleService.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 IAppTitleService { diff --git a/src/EventLogExpert.UI/Services/System/ITitleProvider.cs b/src/EventLogExpert.UI/Common/AppTitle/ITitleProvider.cs similarity index 79% rename from src/EventLogExpert.UI/Services/System/ITitleProvider.cs rename to src/EventLogExpert.UI/Common/AppTitle/ITitleProvider.cs index 6956da10..ce773737 100644 --- a/src/EventLogExpert.UI/Services/System/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/Services/Deployment/DeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs index dd9435e5..22f9065f 100644 --- a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using System.Reflection; diff --git a/src/EventLogExpert.UI/Services/Update/UpdateService.cs b/src/EventLogExpert.UI/Services/Update/UpdateService.cs index 3e29a732..5a3bc997 100644 --- a/src/EventLogExpert.UI/Services/Update/UpdateService.cs +++ b/src/EventLogExpert.UI/Services/Update/UpdateService.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert/App.xaml.cs b/src/EventLogExpert/App.xaml.cs index 8f5c9fe1..1750ef7d 100644 --- a/src/EventLogExpert/App.xaml.cs +++ b/src/EventLogExpert/App.xaml.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; using EventLogExpert.UI; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs index 5aeab89d..9f99c567 100644 --- a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs +++ b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Services; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert/MainPage.xaml.cs b/src/EventLogExpert/MainPage.xaml.cs index ab4939b1..c121f1c5 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.cs @@ -5,6 +5,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; using EventLogExpert.UI; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 78f48116..855f862e 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Services; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Common.Preferences; diff --git a/src/EventLogExpert/Services/TitleProvider.cs b/src/EventLogExpert/Services/TitleProvider.cs index 78fea865..8e7e89e6 100644 --- a/src/EventLogExpert/Services/TitleProvider.cs +++ b/src/EventLogExpert/Services/TitleProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.AppTitle; namespace EventLogExpert.Services; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs index 21fbca85..fca8ff0c 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs index 3e1f47af..f6f2a5ef 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/System/AppTitleServiceTests.cs @@ -1,9 +1,8 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; using NSubstitute; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs index deefae3b..517f560e 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; From c2afc031b5068348900f9555fafd576564e4f9ca Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 18:52:20 -0500 Subject: [PATCH 06/33] Move IMainThreadService to Common/Threading/ --- .../{Services/System => Common/Threading}/IMainThreadService.cs | 2 +- .../Services/Alerts/ModalAlertDialogService.cs | 1 + src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs | 1 + src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/MauiMainThreadService.cs | 2 +- .../Services/Alerts/ModalAlertDialogServiceTests.cs | 1 + .../Services/Deployment/DeploymentServiceTests.cs | 1 + 7 files changed, 7 insertions(+), 2 deletions(-) rename src/EventLogExpert.UI/{Services/System => Common/Threading}/IMainThreadService.cs (87%) diff --git a/src/EventLogExpert.UI/Services/System/IMainThreadService.cs b/src/EventLogExpert.UI/Common/Threading/IMainThreadService.cs similarity index 87% rename from src/EventLogExpert.UI/Services/System/IMainThreadService.cs rename to src/EventLogExpert.UI/Common/Threading/IMainThreadService.cs index f4a514bb..500715a2 100644 --- a/src/EventLogExpert.UI/Services/System/IMainThreadService.cs +++ b/src/EventLogExpert.UI/Common/Threading/IMainThreadService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Threading; public interface IMainThreadService { diff --git a/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs b/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs index ae0b0997..5544ca4e 100644 --- a/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs +++ b/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs index 22f9065f..3257eb87 100644 --- a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using System.Reflection; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 855f862e..7523654c 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -10,6 +10,7 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Common.Preferences; +using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert/Services/MauiMainThreadService.cs b/src/EventLogExpert/Services/MauiMainThreadService.cs index c82e35a4..fc4f92df 100644 --- a/src/EventLogExpert/Services/MauiMainThreadService.cs +++ b/src/EventLogExpert/Services/MauiMainThreadService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Common.Threading; namespace EventLogExpert.Services; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs index b43a24a6..8fc6805b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs index fca8ff0c..b75fc989 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs @@ -3,6 +3,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; From 235f048940e4c73b20c12271957ea0e64826230d Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 19:00:27 -0500 Subject: [PATCH 07/33] Move IWindowsIdentityProvider to Common/Identity/ --- .../System => Common/Identity}/IWindowsIdentityProvider.cs | 2 +- .../Common/Versioning/CurrentVersionProvider.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/WindowsIdentityProvider.cs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename src/EventLogExpert.UI/{Services/System => Common/Identity}/IWindowsIdentityProvider.cs (78%) diff --git a/src/EventLogExpert.UI/Services/System/IWindowsIdentityProvider.cs b/src/EventLogExpert.UI/Common/Identity/IWindowsIdentityProvider.cs similarity index 78% rename from src/EventLogExpert.UI/Services/System/IWindowsIdentityProvider.cs rename to src/EventLogExpert.UI/Common/Identity/IWindowsIdentityProvider.cs index c176dd2e..ed74df93 100644 --- a/src/EventLogExpert.UI/Services/System/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/Common/Versioning/CurrentVersionProvider.cs b/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs index 67d00d3e..789ea339 100644 --- a/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs +++ b/src/EventLogExpert.UI/Common/Versioning/CurrentVersionProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Identity; namespace EventLogExpert.UI.Common.Versioning; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 7523654c..59481ba6 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -9,6 +9,7 @@ using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.Common.Identity; using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Common.Versioning; diff --git a/src/EventLogExpert/Services/WindowsIdentityProvider.cs b/src/EventLogExpert/Services/WindowsIdentityProvider.cs index f97091e1..e90d5b90 100644 --- a/src/EventLogExpert/Services/WindowsIdentityProvider.cs +++ b/src/EventLogExpert/Services/WindowsIdentityProvider.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Identity; using System.Security.Principal; namespace EventLogExpert.Services; From 2fdf11324241f88b5a5cb4e64168bdd3d3e24e7e Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 19:11:01 -0500 Subject: [PATCH 08/33] Move LoggingMiddleware to Common/Logging/ --- .../{Store => Common/Logging}/LoggingMiddleware.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/EventLogExpert.UI/{Store => Common/Logging}/LoggingMiddleware.cs (98%) diff --git a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs b/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs similarity index 98% rename from src/EventLogExpert.UI/Store/LoggingMiddleware.cs rename to src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs index e0e4dcdc..3f8627d9 100644 --- a/src/EventLogExpert.UI/Store/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs @@ -8,7 +8,7 @@ using Fluxor; using System.Text.Json; -namespace EventLogExpert.UI.Store; +namespace EventLogExpert.UI.Common.Logging; public sealed class LoggingMiddleware(ITraceLogger debugLogger) : Middleware { diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 59481ba6..2d2b3731 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -10,13 +10,13 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Common.Identity; +using EventLogExpert.UI.Common.Logging; using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; -using EventLogExpert.UI.Store; using EventLogExpert.UI.Store.EventLog; using Fluxor; From e827258a1abc41589907bebe978f98dcbfc69cfc Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 19:21:13 -0500 Subject: [PATCH 09/33] Move DisplayExtensions to Common/Display/ --- src/EventLogExpert.Components/_Imports.razor | 1 + .../{Extensions => Common/Display}/DisplayExtensions.cs | 2 +- src/EventLogExpert/Components/Sections/EventTable.razor.cs | 1 + src/EventLogExpert/Components/Sections/FilterPane.razor.cs | 1 + src/EventLogExpert/Services/ClipboardService.cs | 1 + src/EventLogExpert/_Imports.razor | 1 + 6 files changed, 6 insertions(+), 1 deletion(-) rename src/EventLogExpert.UI/{Extensions => Common/Display}/DisplayExtensions.cs (95%) diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 5ab39b3e..4bab3d80 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -9,6 +9,7 @@ @using EventLogExpert.Components.Menu @using EventLogExpert.Components.Modals @using EventLogExpert.Components.Modals.Alerts +@using EventLogExpert.UI.Common.Display @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Extensions/DisplayExtensions.cs b/src/EventLogExpert.UI/Common/Display/DisplayExtensions.cs similarity index 95% rename from src/EventLogExpert.UI/Extensions/DisplayExtensions.cs rename to src/EventLogExpert.UI/Common/Display/DisplayExtensions.cs index 4cc7f345..59607fc0 100644 --- a/src/EventLogExpert.UI/Extensions/DisplayExtensions.cs +++ b/src/EventLogExpert.UI/Common/Display/DisplayExtensions.cs @@ -4,7 +4,7 @@ using System.Reflection; using System.Runtime.Serialization; -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Common.Display; public static class DisplayExtensions { diff --git a/src/EventLogExpert/Components/Sections/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index eec96045..805ddd02 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -5,6 +5,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Display; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs index f23f57ed..64dd9805 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Modals.Filters; using EventLogExpert.UI; +using EventLogExpert.UI.Common.Display; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index 65e297d8..dd152db7 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Display; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index 53052354..01f7f556 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -19,6 +19,7 @@ @using EventLogExpert.Components.Sections @using EventLogExpert.Eventing.Readers @using EventLogExpert.UI +@using EventLogExpert.UI.Common.Display @using EventLogExpert.UI.Models @using Fluxor From 9abb2b4c94eb0c11517cd13e075290aa99d925a5 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 19:41:05 -0500 Subject: [PATCH 10/33] Promote StatusBar slice --- src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs | 2 +- .../{Store => }/StatusBar/ClearStatusAction.cs | 2 +- src/EventLogExpert.UI/{Store => }/StatusBar/CloseAllAction.cs | 2 +- src/EventLogExpert.UI/{Store => }/StatusBar/Reducers.cs | 2 +- .../{Store => }/StatusBar/SetEventsLoadingAction.cs | 2 +- .../{Store => }/StatusBar/SetResolverStatusAction.cs | 2 +- src/EventLogExpert.UI/{Store => }/StatusBar/StatusBarState.cs | 4 ++-- src/EventLogExpert.UI/Store/EventLog/Effects.cs | 2 +- src/EventLogExpert/Components/Sections/StatusBar.razor.cs | 2 +- .../{Store => }/StatusBar/StatusBarStoreTests.cs | 4 ++-- .../EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs | 4 ++-- 11 files changed, 14 insertions(+), 14 deletions(-) rename src/EventLogExpert.UI/{Store => }/StatusBar/ClearStatusAction.cs (75%) rename src/EventLogExpert.UI/{Store => }/StatusBar/CloseAllAction.cs (72%) rename src/EventLogExpert.UI/{Store => }/StatusBar/Reducers.cs (97%) rename src/EventLogExpert.UI/{Store => }/StatusBar/SetEventsLoadingAction.cs (91%) rename src/EventLogExpert.UI/{Store => }/StatusBar/SetResolverStatusAction.cs (77%) rename src/EventLogExpert.UI/{Store => }/StatusBar/StatusBarState.cs (80%) rename tests/Unit/EventLogExpert.UI.Tests/{Store => }/StatusBar/StatusBarStoreTests.cs (99%) diff --git a/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs b/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs index 3f8627d9..0a9a46e1 100644 --- a/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs +++ b/src/EventLogExpert.UI/Common/Logging/LoggingMiddleware.cs @@ -2,9 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.StatusBar; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; -using EventLogExpert.UI.Store.StatusBar; using Fluxor; using System.Text.Json; diff --git a/src/EventLogExpert.UI/Store/StatusBar/ClearStatusAction.cs b/src/EventLogExpert.UI/StatusBar/ClearStatusAction.cs similarity index 75% rename from src/EventLogExpert.UI/Store/StatusBar/ClearStatusAction.cs rename to src/EventLogExpert.UI/StatusBar/ClearStatusAction.cs index da24c413..aba72124 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/ClearStatusAction.cs +++ b/src/EventLogExpert.UI/StatusBar/ClearStatusAction.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; public sealed record ClearStatusAction(Guid ActivityId); diff --git a/src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs b/src/EventLogExpert.UI/StatusBar/CloseAllAction.cs similarity index 72% rename from src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs rename to src/EventLogExpert.UI/StatusBar/CloseAllAction.cs index 734813b1..33333f88 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/CloseAllAction.cs +++ b/src/EventLogExpert.UI/StatusBar/CloseAllAction.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; public sealed record CloseAllAction; diff --git a/src/EventLogExpert.UI/Store/StatusBar/Reducers.cs b/src/EventLogExpert.UI/StatusBar/Reducers.cs similarity index 97% rename from src/EventLogExpert.UI/Store/StatusBar/Reducers.cs rename to src/EventLogExpert.UI/StatusBar/Reducers.cs index f3385480..e5a70507 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/Reducers.cs +++ b/src/EventLogExpert.UI/StatusBar/Reducers.cs @@ -4,7 +4,7 @@ using Fluxor; using System.Collections.Immutable; -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; public sealed class Reducers { diff --git a/src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs b/src/EventLogExpert.UI/StatusBar/SetEventsLoadingAction.cs similarity index 91% rename from src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs rename to src/EventLogExpert.UI/StatusBar/SetEventsLoadingAction.cs index e60f6b97..7563303c 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/SetEventsLoadingAction.cs +++ b/src/EventLogExpert.UI/StatusBar/SetEventsLoadingAction.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; /// Used to indicate the progress of event logs being loaded. /// diff --git a/src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs b/src/EventLogExpert.UI/StatusBar/SetResolverStatusAction.cs similarity index 77% rename from src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs rename to src/EventLogExpert.UI/StatusBar/SetResolverStatusAction.cs index 5dbd7f2a..639b033f 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/SetResolverStatusAction.cs +++ b/src/EventLogExpert.UI/StatusBar/SetResolverStatusAction.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Store.StatusBar; +namespace EventLogExpert.UI.StatusBar; public sealed record SetResolverStatusAction(string ResolverStatus); diff --git a/src/EventLogExpert.UI/Store/StatusBar/StatusBarState.cs b/src/EventLogExpert.UI/StatusBar/StatusBarState.cs similarity index 80% rename from src/EventLogExpert.UI/Store/StatusBar/StatusBarState.cs rename to src/EventLogExpert.UI/StatusBar/StatusBarState.cs index 7a10c607..f1e9f798 100644 --- a/src/EventLogExpert.UI/Store/StatusBar/StatusBarState.cs +++ b/src/EventLogExpert.UI/StatusBar/StatusBarState.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.StatusBar; +namespace EventLogExpert.UI.StatusBar; [FeatureState(MaximumStateChangedNotificationsPerSecond = 1)] public sealed record StatusBarState diff --git a/src/EventLogExpert.UI/Store/EventLog/Effects.cs b/src/EventLogExpert.UI/Store/EventLog/Effects.cs index 84717118..51fd3f1d 100644 --- a/src/EventLogExpert.UI/Store/EventLog/Effects.cs +++ b/src/EventLogExpert.UI/Store/EventLog/Effects.cs @@ -8,9 +8,9 @@ using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.StatusBar; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.FilterPane; -using EventLogExpert.UI.Store.StatusBar; using Fluxor; using Microsoft.Extensions.DependencyInjection; using System.Collections.Concurrent; diff --git a/src/EventLogExpert/Components/Sections/StatusBar.razor.cs b/src/EventLogExpert/Components/Sections/StatusBar.razor.cs index 84b29fab..69d85f0b 100644 --- a/src/EventLogExpert/Components/Sections/StatusBar.razor.cs +++ b/src/EventLogExpert/Components/Sections/StatusBar.razor.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.StatusBar; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.FilterPane; -using EventLogExpert.UI.Store.StatusBar; using Fluxor; using Microsoft.AspNetCore.Components; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs b/tests/Unit/EventLogExpert.UI.Tests/StatusBar/StatusBarStoreTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/StatusBar/StatusBarStoreTests.cs index 9879b1a5..c03dd7ee 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/StatusBar/StatusBarStoreTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/StatusBar/StatusBarStoreTests.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Store.StatusBar; +using EventLogExpert.UI.StatusBar; using System.Collections.Immutable; -namespace EventLogExpert.UI.Tests.Store.StatusBar; +namespace EventLogExpert.UI.Tests.StatusBar; public sealed class StatusBarStateTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs index e8c90f93..7e7011db 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs @@ -8,10 +8,10 @@ using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.StatusBar; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.FilterPane; -using EventLogExpert.UI.Store.StatusBar; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; using Fluxor; @@ -190,7 +190,7 @@ public async Task HandleCloseAll_ShouldRemoveAllLogsAndClearCache() await mockLogWatcher.Received(1).RemoveAllAsync(); mockResolverCache.Received(1).ClearAll(); mockDispatcher.Received(1).Dispatch(Arg.Any()); - mockDispatcher.Received(1).Dispatch(Arg.Any()); + mockDispatcher.Received(1).Dispatch(Arg.Any()); } [Fact] From 8c2e323e00e14a08fb8f69b8f68cb123b88d2422 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 20:08:13 -0500 Subject: [PATCH 11/33] Promote DebugLog slice --- src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs | 2 +- src/EventLogExpert.UI/{Models => }/DebugLog/DebugLogEntry.cs | 2 +- .../{Services => }/DebugLog/DebugLogEntryParser.cs | 3 +-- .../{Services => }/DebugLog/DebugLogProjection.cs | 3 +-- .../{Services => }/DebugLog/DebugLogService.cs | 2 +- src/EventLogExpert.UI/{Services => }/DebugLog/IFileLogger.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 1 + .../Modals/DebugLogModalTests.cs | 1 + .../{Services => }/DebugLog/DebugLogEntryParserTests.cs | 4 ++-- .../{Services => }/DebugLog/DebugLogProjectionTests.cs | 4 ++-- .../{Services => }/DebugLog/DebugLogServiceTests.cs | 4 ++-- 11 files changed, 14 insertions(+), 14 deletions(-) rename src/EventLogExpert.UI/{Models => }/DebugLog/DebugLogEntry.cs (89%) rename src/EventLogExpert.UI/{Services => }/DebugLog/DebugLogEntryParser.cs (98%) rename src/EventLogExpert.UI/{Services => }/DebugLog/DebugLogProjection.cs (97%) rename src/EventLogExpert.UI/{Services => }/DebugLog/DebugLogService.cs (99%) rename src/EventLogExpert.UI/{Services => }/DebugLog/IFileLogger.cs (85%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/DebugLog/DebugLogEntryParserTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/DebugLog/DebugLogProjectionTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/DebugLog/DebugLogServiceTests.cs (99%) diff --git a/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs b/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs index 5cb507c4..91aa3f3c 100644 --- a/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs @@ -5,8 +5,8 @@ using EventLogExpert.UI; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; diff --git a/src/EventLogExpert.UI/Models/DebugLog/DebugLogEntry.cs b/src/EventLogExpert.UI/DebugLog/DebugLogEntry.cs similarity index 89% rename from src/EventLogExpert.UI/Models/DebugLog/DebugLogEntry.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogEntry.cs index fbeef451..2af12e90 100644 --- a/src/EventLogExpert.UI/Models/DebugLog/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/DebugLog/DebugLogEntryParser.cs b/src/EventLogExpert.UI/DebugLog/DebugLogEntryParser.cs similarity index 98% rename from src/EventLogExpert.UI/Services/DebugLog/DebugLogEntryParser.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogEntryParser.cs index 09911fd2..53230b6d 100644 --- a/src/EventLogExpert.UI/Services/DebugLog/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/DebugLog/DebugLogProjection.cs b/src/EventLogExpert.UI/DebugLog/DebugLogProjection.cs similarity index 97% rename from src/EventLogExpert.UI/Services/DebugLog/DebugLogProjection.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogProjection.cs index f041d09c..5de6d5c3 100644 --- a/src/EventLogExpert.UI/Services/DebugLog/DebugLogProjection.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogProjection.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.DebugLog; public static class DebugLogProjection { diff --git a/src/EventLogExpert.UI/Services/DebugLog/DebugLogService.cs b/src/EventLogExpert.UI/DebugLog/DebugLogService.cs similarity index 99% rename from src/EventLogExpert.UI/Services/DebugLog/DebugLogService.cs rename to src/EventLogExpert.UI/DebugLog/DebugLogService.cs index d38d8d1a..23a7befe 100644 --- a/src/EventLogExpert.UI/Services/DebugLog/DebugLogService.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogService.cs @@ -9,7 +9,7 @@ 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/Services/DebugLog/IFileLogger.cs b/src/EventLogExpert.UI/DebugLog/IFileLogger.cs similarity index 85% rename from src/EventLogExpert.UI/Services/DebugLog/IFileLogger.cs rename to src/EventLogExpert.UI/DebugLog/IFileLogger.cs index 54f2f036..a73243c6 100644 --- a/src/EventLogExpert.UI/Services/DebugLog/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/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 2d2b3731..6d32fac7 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -14,6 +14,7 @@ using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Common.Versioning; +using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs index 7450fe13..35a5bf38 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs @@ -7,6 +7,7 @@ using EventLogExpert.Components.Tests.TestUtils.Constants; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; +using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; using Fluxor; using Microsoft.AspNetCore.Components; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogEntryParserTests.cs b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogEntryParserTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogEntryParserTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogEntryParserTests.cs index 04164690..963d3a31 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogEntryParserTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogEntryParserTests.cs @@ -1,12 +1,12 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; +using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; using System.Globalization; -namespace EventLogExpert.UI.Tests.Services.DebugLog; +namespace EventLogExpert.UI.Tests.DebugLog; public sealed class DebugLogEntryParserTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogProjectionTests.cs b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogProjectionTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogProjectionTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogProjectionTests.cs index 5f28d795..41eda716 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogProjectionTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogProjectionTests.cs @@ -1,12 +1,12 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Tests.Services.DebugLog; +namespace EventLogExpert.UI.Tests.DebugLog; public sealed class DebugLogProjectionTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs index 551d1fbf..4689b66e 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/DebugLog/DebugLogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs @@ -1,14 +1,14 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services.DebugLog; +namespace EventLogExpert.UI.Tests.DebugLog; public sealed class DebugLogServiceTests : IDisposable { From 59371d0d1b717ab806b0135a7fd24e5c2f2ff583 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 20:26:01 -0500 Subject: [PATCH 12/33] Promote Banner slice --- src/EventLogExpert.Components/BannerHost.razor.cs | 2 +- .../Database/DatabaseRecoveryDialog.razor.cs | 1 + .../Database/DatabaseRecoveryHost.razor.cs | 1 + .../Database/SettingsUpgradeProgressBanner.razor.cs | 3 +-- src/EventLogExpert.Components/Modals/SettingsModal.razor.cs | 1 + src/EventLogExpert.Components/_Imports.razor | 1 + src/EventLogExpert.UI/{Models => }/Banner/BannerInfoEntry.cs | 2 +- .../{Models => }/Banner/BannerProgressEntry.cs | 4 +++- src/EventLogExpert.UI/{Services => }/Banner/BannerService.cs | 2 +- src/EventLogExpert.UI/{Models => }/Banner/BannerSeverity.cs | 2 +- .../{Services => }/Banner/BannerViewSelector.cs | 2 +- src/EventLogExpert.UI/{Models => }/Banner/ErrorBannerEntry.cs | 2 +- src/EventLogExpert.UI/{Services => }/Banner/IBannerService.cs | 2 +- src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs | 1 + .../Services/Alerts/ModalAlertDialogService.cs | 2 +- src/EventLogExpert.UI/Store/EventLog/Effects.cs | 1 + .../Components/Layout/UnhandledExceptionHandler.razor.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/_Imports.razor | 3 ++- tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs | 1 + .../Database/DatabaseRecoveryDialogTests.cs | 1 + .../Database/DatabaseRecoveryHostTests.cs | 1 + .../Database/SettingsUpgradeProgressBannerTests.cs | 2 +- .../{Services => }/Banner/BannerServiceTests.cs | 4 ++-- .../{Services => }/Banner/BannerViewSelectorTests.cs | 4 ++-- .../Services/Alerts/ModalAlertDialogServiceTests.cs | 2 +- .../EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs | 1 + 27 files changed, 32 insertions(+), 19 deletions(-) rename src/EventLogExpert.UI/{Models => }/Banner/BannerInfoEntry.cs (86%) rename src/EventLogExpert.UI/{Models => }/Banner/BannerProgressEntry.cs (83%) rename src/EventLogExpert.UI/{Services => }/Banner/BannerService.cs (99%) rename src/EventLogExpert.UI/{Models => }/Banner/BannerSeverity.cs (79%) rename src/EventLogExpert.UI/{Services => }/Banner/BannerViewSelector.cs (98%) rename src/EventLogExpert.UI/{Models => }/Banner/ErrorBannerEntry.cs (87%) rename src/EventLogExpert.UI/{Services => }/Banner/IBannerService.cs (96%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Banner/BannerServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Banner/BannerViewSelectorTests.cs (99%) diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index c88261ab..b3077720 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -2,10 +2,10 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs index ef01a0b0..198ab366 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs index af9b306c..51b36765 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using Microsoft.AspNetCore.Components; 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/Modals/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index 5a634e25..33495597 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 4bab3d80..52c58889 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -10,6 +10,7 @@ @using EventLogExpert.Components.Modals @using EventLogExpert.Components.Modals.Alerts @using EventLogExpert.UI.Common.Display +@using EventLogExpert.UI.Banner @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Models/Banner/BannerInfoEntry.cs b/src/EventLogExpert.UI/Banner/BannerInfoEntry.cs similarity index 86% rename from src/EventLogExpert.UI/Models/Banner/BannerInfoEntry.cs rename to src/EventLogExpert.UI/Banner/BannerInfoEntry.cs index 983df6cb..0d68645b 100644 --- a/src/EventLogExpert.UI/Models/Banner/BannerInfoEntry.cs +++ b/src/EventLogExpert.UI/Banner/BannerInfoEntry.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Banner; public sealed record BannerInfoEntry( Guid Id, diff --git a/src/EventLogExpert.UI/Models/Banner/BannerProgressEntry.cs b/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs similarity index 83% rename from src/EventLogExpert.UI/Models/Banner/BannerProgressEntry.cs rename to src/EventLogExpert.UI/Banner/BannerProgressEntry.cs index f45b1b71..75537956 100644 --- a/src/EventLogExpert.UI/Models/Banner/BannerProgressEntry.cs +++ b/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs @@ -1,7 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +using EventLogExpert.UI.Models; + +namespace EventLogExpert.UI.Banner; public sealed record BannerProgressEntry( Guid BatchId, diff --git a/src/EventLogExpert.UI/Services/Banner/BannerService.cs b/src/EventLogExpert.UI/Banner/BannerService.cs similarity index 99% rename from src/EventLogExpert.UI/Services/Banner/BannerService.cs rename to src/EventLogExpert.UI/Banner/BannerService.cs index 829f41d8..ed9824bd 100644 --- a/src/EventLogExpert.UI/Services/Banner/BannerService.cs +++ b/src/EventLogExpert.UI/Banner/BannerService.cs @@ -6,7 +6,7 @@ using EventLogExpert.UI.Models; using System.Collections.Immutable; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Banner; public sealed class BannerService : IBannerService { diff --git a/src/EventLogExpert.UI/Models/Banner/BannerSeverity.cs b/src/EventLogExpert.UI/Banner/BannerSeverity.cs similarity index 79% rename from src/EventLogExpert.UI/Models/Banner/BannerSeverity.cs rename to src/EventLogExpert.UI/Banner/BannerSeverity.cs index eddf50bc..4d4ad641 100644 --- a/src/EventLogExpert.UI/Models/Banner/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/Banner/BannerViewSelector.cs b/src/EventLogExpert.UI/Banner/BannerViewSelector.cs similarity index 98% rename from src/EventLogExpert.UI/Services/Banner/BannerViewSelector.cs rename to src/EventLogExpert.UI/Banner/BannerViewSelector.cs index dd67d16f..69ce8d08 100644 --- a/src/EventLogExpert.UI/Services/Banner/BannerViewSelector.cs +++ b/src/EventLogExpert.UI/Banner/BannerViewSelector.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Models; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Banner; public enum BannerView { diff --git a/src/EventLogExpert.UI/Models/Banner/ErrorBannerEntry.cs b/src/EventLogExpert.UI/Banner/ErrorBannerEntry.cs similarity index 87% rename from src/EventLogExpert.UI/Models/Banner/ErrorBannerEntry.cs rename to src/EventLogExpert.UI/Banner/ErrorBannerEntry.cs index a6327afd..704798f4 100644 --- a/src/EventLogExpert.UI/Models/Banner/ErrorBannerEntry.cs +++ b/src/EventLogExpert.UI/Banner/ErrorBannerEntry.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Models; +namespace EventLogExpert.UI.Banner; public sealed record ErrorBannerEntry( Guid Id, diff --git a/src/EventLogExpert.UI/Services/Banner/IBannerService.cs b/src/EventLogExpert.UI/Banner/IBannerService.cs similarity index 96% rename from src/EventLogExpert.UI/Services/Banner/IBannerService.cs rename to src/EventLogExpert.UI/Banner/IBannerService.cs index 0b53c7fc..fd871d75 100644 --- a/src/EventLogExpert.UI/Services/Banner/IBannerService.cs +++ b/src/EventLogExpert.UI/Banner/IBannerService.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Models; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Banner; public interface IBannerService { diff --git a/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs b/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs index c3afa7e0..656153f2 100644 --- a/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs +++ b/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs @@ -1,6 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; namespace EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs b/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs index 5544ca4e..408bb49c 100644 --- a/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs +++ b/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; namespace EventLogExpert.UI.Services; diff --git a/src/EventLogExpert.UI/Store/EventLog/Effects.cs b/src/EventLogExpert.UI/Store/EventLog/Effects.cs index 51fd3f1d..4e0899f9 100644 --- a/src/EventLogExpert.UI/Store/EventLog/Effects.cs +++ b/src/EventLogExpert.UI/Store/EventLog/Effects.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Resolvers; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.StatusBar; 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/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 6d32fac7..8b3eb3b0 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Services; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index 01f7f556..6ae9d55b 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -1,4 +1,4 @@ -@using System.Net.Http +@using System.Net.Http @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @@ -20,6 +20,7 @@ @using EventLogExpert.Eventing.Readers @using EventLogExpert.UI @using EventLogExpert.UI.Common.Display +@using EventLogExpert.UI.Banner @using EventLogExpert.UI.Models @using Fluxor diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs index bece5f9c..4a7de279 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -4,6 +4,7 @@ using Bunit; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs index b5369c55..f9b37ecb 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs @@ -7,6 +7,7 @@ using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs index a70aa161..5042909f 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs @@ -5,6 +5,7 @@ using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components.Web; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs index e92ee3a5..6c991798 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs @@ -4,7 +4,7 @@ using Bunit; using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs index d0a65546..46b758fa 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs @@ -2,12 +2,12 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services.Banner; +namespace EventLogExpert.UI.Tests.Banner; public sealed class BannerServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerViewSelectorTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerViewSelectorTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs index 64ab4b72..c38d5cbc 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Banner/BannerViewSelectorTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Tests.Services.Banner; +namespace EventLogExpert.UI.Tests.Banner; public sealed class BannerViewSelectorTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs index 8fc6805b..5cafde77 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; using EventLogExpert.UI.Services; using NSubstitute; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs index 7e7011db..4dc77cf8 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Resolvers; +using EventLogExpert.UI.Banner; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.StatusBar; From c94e60fcbb6ae6eb85fda1ed7ba0f4bb0f0901bf Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 20:32:11 -0500 Subject: [PATCH 13/33] Promote Modal slice --- src/EventLogExpert.Components/Base/ModalBase.cs | 1 + src/EventLogExpert.Components/Modals/ModalHost.razor.cs | 2 +- src/EventLogExpert.UI/{Services => }/Modal/IModalService.cs | 2 +- src/EventLogExpert.UI/{Services => }/Modal/ModalService.cs | 3 +-- .../Services/Alerts/InlineAlertHostBroker.cs | 1 + src/EventLogExpert/Components/Sections/FilterPane.razor.cs | 1 + src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/KeyboardShortcutService.cs | 1 + src/EventLogExpert/Services/MauiMenuActionService.cs | 1 + .../Modals/DebugLogModalTests.cs | 1 + .../{Services => }/Modal/ModalServiceTests.cs | 4 ++-- .../Services/Alerts/InlineAlertHostBrokerTests.cs | 1 + 12 files changed, 13 insertions(+), 6 deletions(-) rename src/EventLogExpert.UI/{Services => }/Modal/IModalService.cs (96%) rename src/EventLogExpert.UI/{Services => }/Modal/ModalService.cs (97%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Modal/ModalServiceTests.cs (98%) diff --git a/src/EventLogExpert.Components/Base/ModalBase.cs b/src/EventLogExpert.Components/Base/ModalBase.cs index 3c80fea5..804e0037 100644 --- a/src/EventLogExpert.Components/Base/ModalBase.cs +++ b/src/EventLogExpert.Components/Base/ModalBase.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using Fluxor.Blazor.Web.Components; using Microsoft.AspNetCore.Components; 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.UI/Services/Modal/IModalService.cs b/src/EventLogExpert.UI/Modal/IModalService.cs similarity index 96% rename from src/EventLogExpert.UI/Services/Modal/IModalService.cs rename to src/EventLogExpert.UI/Modal/IModalService.cs index c3e841c7..0983e486 100644 --- a/src/EventLogExpert.UI/Services/Modal/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 diff --git a/src/EventLogExpert.UI/Services/Modal/ModalService.cs b/src/EventLogExpert.UI/Modal/ModalService.cs similarity index 97% rename from src/EventLogExpert.UI/Services/Modal/ModalService.cs rename to src/EventLogExpert.UI/Modal/ModalService.cs index 56222a42..d0ae59d5 100644 --- a/src/EventLogExpert.UI/Services/Modal/ModalService.cs +++ b/src/EventLogExpert.UI/Modal/ModalService.cs @@ -1,10 +1,9 @@ // // 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 { diff --git a/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs b/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs index 0782deef..3a365c26 100644 --- a/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs +++ b/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; namespace EventLogExpert.UI.Services; diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs index 64dd9805..703b9e48 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs @@ -5,6 +5,7 @@ using EventLogExpert.UI; using EventLogExpert.UI.Common.Display; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 8b3eb3b0..a9484936 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -17,6 +17,7 @@ using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/Services/KeyboardShortcutService.cs b/src/EventLogExpert/Services/KeyboardShortcutService.cs index 8c28e34b..17f21f02 100644 --- a/src/EventLogExpert/Services/KeyboardShortcutService.cs +++ b/src/EventLogExpert/Services/KeyboardShortcutService.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using Microsoft.JSInterop; namespace EventLogExpert.Services; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 2b1dd9ab..3acf962e 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -11,6 +11,7 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs index 35a5bf38..acd0da67 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs @@ -9,6 +9,7 @@ using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using Fluxor; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Modal/ModalServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Modal/ModalServiceTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Modal/ModalServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Modal/ModalServiceTests.cs index 67305ef2..a4954ad1 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Modal/ModalServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Modal/ModalServiceTests.cs @@ -1,10 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Modal; using Microsoft.AspNetCore.Components; -namespace EventLogExpert.UI.Tests.Services.Modal; +namespace EventLogExpert.UI.Tests.Modal; public sealed class ModalServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs index ef11065f..1b0436bb 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Modal; using EventLogExpert.UI.Services; using NSubstitute; From 4b253579972ef665b34e5dca2f0df5d22621ae61 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 20:53:06 -0500 Subject: [PATCH 14/33] Promote Alerts slice and rename ModalAlertDialogService to AlertDialogService --- .../Base/ModalBase.cs | 2 +- .../Base/ModalChrome.razor.cs | 2 +- .../Filters/FilterRow.razor.cs | 2 +- .../Modals/Alerts/AlertModal.razor.cs | 2 +- .../Modals/Alerts/PromptModal.razor.cs | 2 +- .../Modals/DebugLogModal.razor.cs | 2 +- .../Modals/Filters/FilterCacheModal.razor.cs | 2 +- .../Modals/Filters/FilterGroup.razor.cs | 2 +- .../Modals/Filters/FilterGroupModal.razor.cs | 2 +- .../Modals/SettingsModal.razor.cs | 1 + src/EventLogExpert.Components/_Imports.razor | 1 + .../AlertDialogService.cs} | 5 +-- .../Alerts/AlertPresentation.cs | 7 ++-- .../Alerts/EmptyLogAlertFormatter.cs | 2 +- .../Alerts/IAlertDialogService.cs | 4 +- .../IInlineAlertHost.cs | 2 +- .../Alerts/IInlineAlertHostBroker.cs | 2 +- .../Alerts/InlineAlertHostBroker.cs | 3 +- .../InlineAlertRequest.cs | 2 +- .../InlineAlertResult.cs | 2 +- .../Common/Files/IFileSaveService.cs | 2 +- .../Services/Deployment/DeploymentService.cs | 1 + .../Services/Update/UpdateService.cs | 1 + src/EventLogExpert/MauiProgram.cs | 3 +- .../Services/MauiMenuActionService.cs | 1 + src/EventLogExpert/_Imports.razor | 1 + .../Modals/DebugLogModalTests.cs | 2 +- .../AlertDialogServiceTests.cs} | 41 +++++++++---------- .../Alerts/EmptyLogAlertFormatterTests.cs | 4 +- .../Alerts/InlineAlertHostBrokerTests.cs | 5 +-- .../Deployment/DeploymentServiceTests.cs | 1 + .../Services/Update/UpdateServiceTests.cs | 1 + 32 files changed, 57 insertions(+), 55 deletions(-) rename src/EventLogExpert.UI/{Services/Alerts/ModalAlertDialogService.cs => Alerts/AlertDialogService.cs} (97%) rename src/EventLogExpert.UI/{Services => }/Alerts/AlertPresentation.cs (77%) rename src/EventLogExpert.UI/{Services => }/Alerts/EmptyLogAlertFormatter.cs (95%) rename src/EventLogExpert.UI/{Services => }/Alerts/IAlertDialogService.cs (90%) rename src/EventLogExpert.UI/{Interfaces => Alerts}/IInlineAlertHost.cs (93%) rename src/EventLogExpert.UI/{Services => }/Alerts/IInlineAlertHostBroker.cs (97%) rename src/EventLogExpert.UI/{Services => }/Alerts/InlineAlertHostBroker.cs (95%) rename src/EventLogExpert.UI/{Interfaces => Alerts}/InlineAlertRequest.cs (92%) rename src/EventLogExpert.UI/{Interfaces => Alerts}/InlineAlertResult.cs (87%) rename tests/Unit/EventLogExpert.UI.Tests/{Services/Alerts/ModalAlertDialogServiceTests.cs => Alerts/AlertDialogServiceTests.cs} (94%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Alerts/EmptyLogAlertFormatterTests.cs (94%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Alerts/InlineAlertHostBrokerTests.cs (97%) diff --git a/src/EventLogExpert.Components/Base/ModalBase.cs b/src/EventLogExpert.Components/Base/ModalBase.cs index 804e0037..976f7a78 100644 --- a/src/EventLogExpert.Components/Base/ModalBase.cs +++ b/src/EventLogExpert.Components/Base/ModalBase.cs @@ -1,7 +1,7 @@ // // 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; diff --git a/src/EventLogExpert.Components/Base/ModalChrome.razor.cs b/src/EventLogExpert.Components/Base/ModalChrome.razor.cs index 8667d3d4..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; diff --git a/src/EventLogExpert.Components/Filters/FilterRow.razor.cs b/src/EventLogExpert.Components/Filters/FilterRow.razor.cs index 002b3490..14877b6f 100644 --- a/src/EventLogExpert.Components/Filters/FilterRow.razor.cs +++ b/src/EventLogExpert.Components/Filters/FilterRow.razor.cs @@ -3,7 +3,7 @@ using EventLogExpert.Components.Filters.Base; using EventLogExpert.UI; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterPane; using Microsoft.AspNetCore.Components; 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.cs b/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs index 91aa3f3c..18ea978f 100644 --- a/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/DebugLogModal.razor.cs @@ -3,10 +3,10 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.DebugLog; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.Logging; diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs index 49a15aee..7feb26a7 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterCacheModal.razor.cs @@ -3,8 +3,8 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Files; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterCache; using EventLogExpert.UI.Store.FilterPane; diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs index 143ac9aa..c63e56ee 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroup.razor.cs @@ -2,9 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.UI; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterGroup; using EventLogExpert.UI.Store.FilterPane; diff --git a/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs index b27ffa7d..aa0572e4 100644 --- a/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/Filters/FilterGroupModal.razor.cs @@ -2,8 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Files; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.Store.FilterGroup; using Fluxor; diff --git a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index 33495597..d0a44b08 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -3,6 +3,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.UI; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 52c58889..5f8300db 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -11,6 +11,7 @@ @using EventLogExpert.Components.Modals.Alerts @using EventLogExpert.UI.Common.Display @using EventLogExpert.UI.Banner +@using EventLogExpert.UI.Alerts @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs b/src/EventLogExpert.UI/Alerts/AlertDialogService.cs similarity index 97% rename from src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs rename to src/EventLogExpert.UI/Alerts/AlertDialogService.cs index 408bb49c..da7f4a35 100644 --- a/src/EventLogExpert.UI/Services/Alerts/ModalAlertDialogService.cs +++ b/src/EventLogExpert.UI/Alerts/AlertDialogService.cs @@ -3,11 +3,10 @@ using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Threading; -using EventLogExpert.UI.Interfaces; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Alerts; -public sealed class ModalAlertDialogService( +public sealed class AlertDialogService( IInlineAlertHostBroker inlineAlertHostBroker, IMainThreadService mainThreadService, IBannerService bannerService, diff --git a/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs b/src/EventLogExpert.UI/Alerts/AlertPresentation.cs similarity index 77% rename from src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs rename to src/EventLogExpert.UI/Alerts/AlertPresentation.cs index 656153f2..e9b4777a 100644 --- a/src/EventLogExpert.UI/Services/Alerts/AlertPresentation.cs +++ b/src/EventLogExpert.UI/Alerts/AlertPresentation.cs @@ -2,9 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Interfaces; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Alerts; /// /// Controls how an request @@ -13,8 +12,8 @@ namespace EventLogExpert.UI.Services; 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. + /// Default. Use the existing routing: render inline in the active modal host if + /// one is registered, otherwise open a standalone alert popup. /// Auto, diff --git a/src/EventLogExpert.UI/Services/Alerts/EmptyLogAlertFormatter.cs b/src/EventLogExpert.UI/Alerts/EmptyLogAlertFormatter.cs similarity index 95% rename from src/EventLogExpert.UI/Services/Alerts/EmptyLogAlertFormatter.cs rename to src/EventLogExpert.UI/Alerts/EmptyLogAlertFormatter.cs index f78b5085..718ef4fd 100644 --- a/src/EventLogExpert.UI/Services/Alerts/EmptyLogAlertFormatter.cs +++ b/src/EventLogExpert.UI/Alerts/EmptyLogAlertFormatter.cs @@ -1,7 +1,7 @@ // // 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. diff --git a/src/EventLogExpert.UI/Services/Alerts/IAlertDialogService.cs b/src/EventLogExpert.UI/Alerts/IAlertDialogService.cs similarity index 90% rename from src/EventLogExpert.UI/Services/Alerts/IAlertDialogService.cs rename to src/EventLogExpert.UI/Alerts/IAlertDialogService.cs index b2650f1d..5a035847 100644 --- a/src/EventLogExpert.UI/Services/Alerts/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/Interfaces/IInlineAlertHost.cs b/src/EventLogExpert.UI/Alerts/IInlineAlertHost.cs similarity index 93% rename from src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs rename to src/EventLogExpert.UI/Alerts/IInlineAlertHost.cs index 0663ebd8..c57c8444 100644 --- a/src/EventLogExpert.UI/Interfaces/IInlineAlertHost.cs +++ b/src/EventLogExpert.UI/Alerts/IInlineAlertHost.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Alerts; /// /// Implemented by an active modal so alerts can be routed as inline banners instead of opening a separate alert diff --git a/src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs b/src/EventLogExpert.UI/Alerts/IInlineAlertHostBroker.cs similarity index 97% rename from src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs rename to src/EventLogExpert.UI/Alerts/IInlineAlertHostBroker.cs index 329935cd..c725dd89 100644 --- a/src/EventLogExpert.UI/Services/Alerts/IInlineAlertHostBroker.cs +++ b/src/EventLogExpert.UI/Alerts/IInlineAlertHostBroker.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Alerts; /// /// Tracks the currently registered so cross-cutting alert dispatchers can route diff --git a/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs b/src/EventLogExpert.UI/Alerts/InlineAlertHostBroker.cs similarity index 95% rename from src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs rename to src/EventLogExpert.UI/Alerts/InlineAlertHostBroker.cs index 3a365c26..e744f105 100644 --- a/src/EventLogExpert.UI/Services/Alerts/InlineAlertHostBroker.cs +++ b/src/EventLogExpert.UI/Alerts/InlineAlertHostBroker.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Modal; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Alerts; public sealed class InlineAlertHostBroker(IModalService modalService) : IInlineAlertHostBroker { diff --git a/src/EventLogExpert.UI/Interfaces/InlineAlertRequest.cs b/src/EventLogExpert.UI/Alerts/InlineAlertRequest.cs similarity index 92% rename from src/EventLogExpert.UI/Interfaces/InlineAlertRequest.cs rename to src/EventLogExpert.UI/Alerts/InlineAlertRequest.cs index 4b41a831..4c1b9e78 100644 --- a/src/EventLogExpert.UI/Interfaces/InlineAlertRequest.cs +++ b/src/EventLogExpert.UI/Alerts/InlineAlertRequest.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Alerts; /// /// Describes an alert/prompt hosted inline by an active modal. AcceptLabel null means dismiss-only; diff --git a/src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs b/src/EventLogExpert.UI/Alerts/InlineAlertResult.cs similarity index 87% rename from src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs rename to src/EventLogExpert.UI/Alerts/InlineAlertResult.cs index 3b4d38aa..44ccb47a 100644 --- a/src/EventLogExpert.UI/Interfaces/InlineAlertResult.cs +++ b/src/EventLogExpert.UI/Alerts/InlineAlertResult.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Interfaces; +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/Common/Files/IFileSaveService.cs b/src/EventLogExpert.UI/Common/Files/IFileSaveService.cs index a6738ed2..f6475931 100644 --- a/src/EventLogExpert.UI/Common/Files/IFileSaveService.cs +++ b/src/EventLogExpert.UI/Common/Files/IFileSaveService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Alerts; namespace EventLogExpert.UI.Common.Files; diff --git a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs index 3257eb87..2c1c9570 100644 --- a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert.UI/Services/Update/UpdateService.cs b/src/EventLogExpert.UI/Services/Update/UpdateService.cs index 5a3bc997..1144f63e 100644 --- a/src/EventLogExpert.UI/Services/Update/UpdateService.cs +++ b/src/EventLogExpert.UI/Services/Update/UpdateService.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index a9484936..7d46c148 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -6,6 +6,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.Services; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Clipboard; @@ -128,7 +129,7 @@ public static MauiApp CreateMauiApp() var mainThreadService = provider.GetRequiredService(); var bannerService = provider.GetRequiredService(); - return new ModalAlertDialogService( + return new AlertDialogService( inlineAlertHostBroker, mainThreadService, bannerService, diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 3acf962e..4f1ef734 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -8,6 +8,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Platforms.Windows; using EventLogExpert.UI; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index 6ae9d55b..fbd32442 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -21,6 +21,7 @@ @using EventLogExpert.UI @using EventLogExpert.UI.Common.Display @using EventLogExpert.UI.Banner +@using EventLogExpert.UI.Alerts @using EventLogExpert.UI.Models @using Fluxor diff --git a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs index acd0da67..041e2f0b 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Modals/DebugLogModalTests.cs @@ -5,10 +5,10 @@ using EventLogExpert.Components.Modals; using EventLogExpert.Components.Tests.TestUtils; using EventLogExpert.Components.Tests.TestUtils.Constants; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.DebugLog; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Modal; using Fluxor; using Microsoft.AspNetCore.Components; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs similarity index 94% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs index 5cafde77..c9196b5c 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/ModalAlertDialogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Alerts/AlertDialogServiceTests.cs @@ -1,13 +1,12 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Threading; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Services; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services.Alerts; +namespace EventLogExpert.UI.Tests.Alerts; public sealed class ModalAlertDialogServiceTests { @@ -26,7 +25,7 @@ public async Task DisplayPrompt_WhenActiveHost_ShouldRouteInlineAndReturnTypedVa return true; }); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -58,7 +57,7 @@ public async Task DisplayPrompt_WhenInlineCancelled_ShouldReturnEmptyString() return true; }); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -80,7 +79,7 @@ public async Task DisplayPrompt_WhenNoActiveHost_ShouldCallStandalonePromptOpene broker.TryGet(out Arg.Any()).Returns(false); IReadOnlyDictionary? capturedPrompt = null; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -105,7 +104,7 @@ public async Task ShowAlertOneButton_BannerPresentation_DoesNotMarshalThroughMai var bannerService = Substitute.For(); var mainThread = Substitute.For(); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( Substitute.For(), mainThread, bannerService, @@ -128,7 +127,7 @@ public async Task ShowAlertOneButton_BannerPresentation_RoutesToReportInfoBanner var broker = Substitute.For(); var standaloneCalled = false; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), bannerService, @@ -151,7 +150,7 @@ public async Task ShowAlertOneButton_InlineOnlyNoHost_ThrowsInvalidOperationExce var broker = Substitute.For(); broker.TryGet(out Arg.Any()).Returns(false); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -179,7 +178,7 @@ public async Task ShowAlertOneButton_InlineOnlyWithHost_RoutesInline() }); var standaloneCalled = false; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -207,7 +206,7 @@ public async Task ShowAlertOneButton_PopupOnly_AlwaysOpensStandalone_EvenWithHos }); IReadOnlyDictionary? capturedAlert = null; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -231,7 +230,7 @@ public async Task ShowAlertOneButton_WhenNoActiveHost_ShouldCallStandaloneOpener broker.TryGet(out Arg.Any()).Returns(false); IReadOnlyDictionary? capturedAlert = null; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -253,7 +252,7 @@ public async Task ShowAlertOneButton_WhenNoActiveHost_ShouldCallStandaloneOpener public async Task ShowAlertTwoButton_BannerPresentation_ThrowsArgumentException() { // Arrange - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( Substitute.For(), PassthroughMainThread(), Substitute.For(), @@ -273,7 +272,7 @@ public async Task ShowAlertTwoButton_InlineOnlyNoHost_ThrowsInvalidOperationExce var broker = Substitute.For(); broker.TryGet(out Arg.Any()).Returns(false); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -298,7 +297,7 @@ public async Task ShowAlertTwoButton_PopupOnly_AlwaysOpensStandalone() }); IReadOnlyDictionary? capturedAlert = null; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -332,7 +331,7 @@ public async Task ShowAlertTwoButton_WhenActiveHost_ShouldRouteToHostInline() }); var standaloneCalled = false; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -370,7 +369,7 @@ public async Task ShowAlertTwoButton_WhenInlineCancelled_ShouldReturnFalse() return true; }); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), Substitute.For(), @@ -395,7 +394,7 @@ public async Task ShowAlert_ShouldMarshalThroughMainThreadService() mainThread.InvokeOnMainThreadAsync(Arg.Any>()) .Returns(call => ((Func)call[0])()); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, mainThread, Substitute.For(), @@ -416,7 +415,7 @@ public async Task ShowErrorAlert_DoesNotMarshalThroughMainThreadService() var bannerService = Substitute.For(); var mainThread = Substitute.For(); - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( Substitute.For(), mainThread, bannerService, @@ -439,7 +438,7 @@ public async Task ShowErrorAlert_RoutesToBannerServiceReportError_WithTitleAndMe var broker = Substitute.For(); var standaloneCalled = false; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( broker, PassthroughMainThread(), bannerService, @@ -462,7 +461,7 @@ public async Task ShowErrorAlert_WithActionLabelAndAction_PassesThroughToBannerS var bannerService = Substitute.For(); Func action = () => Task.CompletedTask; - var sut = new ModalAlertDialogService( + var sut = new AlertDialogService( Substitute.For(), PassthroughMainThread(), bannerService, diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/EmptyLogAlertFormatterTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Alerts/EmptyLogAlertFormatterTests.cs similarity index 94% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/EmptyLogAlertFormatterTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Alerts/EmptyLogAlertFormatterTests.cs index 023a2799..7d023093 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/EmptyLogAlertFormatterTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Alerts/EmptyLogAlertFormatterTests.cs @@ -1,9 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Alerts; -namespace EventLogExpert.UI.Tests.Services.Alerts; +namespace EventLogExpert.UI.Tests.Alerts; public sealed class EmptyLogAlertFormatterTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Alerts/InlineAlertHostBrokerTests.cs similarity index 97% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Alerts/InlineAlertHostBrokerTests.cs index 1b0436bb..a70a5985 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Alerts/InlineAlertHostBrokerTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Alerts/InlineAlertHostBrokerTests.cs @@ -1,12 +1,11 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Modal; -using EventLogExpert.UI.Services; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services.Alerts; +namespace EventLogExpert.UI.Tests.Alerts; public sealed class InlineAlertHostBrokerTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs index b75fc989..c44747a9 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs index 517f560e..02a2c6b2 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs @@ -2,6 +2,7 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; +using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; From 31d73a0b529b5679af3f7eaf66c89446c9f35186 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 21:02:54 -0500 Subject: [PATCH 15/33] Promote Settings slice --- src/EventLogExpert.Components/Menu/MenuBar.razor.cs | 1 + src/EventLogExpert.Components/Modals/SettingsModal.razor.cs | 1 + src/EventLogExpert.Components/_Imports.razor | 1 + .../Common/Preferences/IPreferencesProvider.cs | 1 + src/EventLogExpert.UI/DebugLog/DebugLogService.cs | 4 ++-- .../{Services => }/Settings/ISettingsService.cs | 4 ++-- .../{Services => }/Settings/SettingsService.cs | 3 +-- src/EventLogExpert.UI/{Enums => Settings}/Theme.cs | 2 +- src/EventLogExpert/App.xaml.cs | 3 +-- src/EventLogExpert/Components/Layout/MainLayout.razor.cs | 2 +- src/EventLogExpert/Components/Sections/DetailsPane.razor.cs | 2 +- src/EventLogExpert/Components/Sections/EventTable.razor.cs | 1 + src/EventLogExpert/Components/Sections/FilterPane.razor.cs | 2 +- src/EventLogExpert/MainPage.xaml.cs | 3 +-- src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/ClipboardService.cs | 2 +- src/EventLogExpert/Services/KeyboardShortcutService.cs | 1 + src/EventLogExpert/Services/MauiMenuActionService.cs | 1 + src/EventLogExpert/Services/PreferencesProvider.cs | 1 + src/EventLogExpert/_Imports.razor | 1 + .../EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs | 2 +- .../{Services => }/Settings/SettingsServiceTests.cs | 4 ++-- 22 files changed, 25 insertions(+), 18 deletions(-) rename src/EventLogExpert.UI/{Services => }/Settings/ISettingsService.cs (88%) rename src/EventLogExpert.UI/{Services => }/Settings/SettingsService.cs (97%) rename src/EventLogExpert.UI/{Enums => Settings}/Theme.cs (78%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Settings/SettingsServiceTests.cs (99%) diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index e41d0ba6..87b95582 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -6,6 +6,7 @@ using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; using Fluxor; diff --git a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index d0a44b08..24edc1e1 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -9,6 +9,7 @@ using EventLogExpert.UI.Common.Files; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using Fluxor; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 5f8300db..fb56d366 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -12,6 +12,7 @@ @using EventLogExpert.UI.Common.Display @using EventLogExpert.UI.Banner @using EventLogExpert.UI.Alerts +@using EventLogExpert.UI.Settings @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs b/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs index 4e47975f..ad02f94b 100644 --- a/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs +++ b/src/EventLogExpert.UI/Common/Preferences/IPreferencesProvider.cs @@ -3,6 +3,7 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using Microsoft.Extensions.Logging; namespace EventLogExpert.UI.Common.Preferences; diff --git a/src/EventLogExpert.UI/DebugLog/DebugLogService.cs b/src/EventLogExpert.UI/DebugLog/DebugLogService.cs index 23a7befe..d41a2ece 100644 --- a/src/EventLogExpert.UI/DebugLog/DebugLogService.cs +++ b/src/EventLogExpert.UI/DebugLog/DebugLogService.cs @@ -1,9 +1,9 @@ -// // 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.Settings; using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; using System.Security.Cryptography; diff --git a/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs b/src/EventLogExpert.UI/Settings/ISettingsService.cs similarity index 88% rename from src/EventLogExpert.UI/Services/Settings/ISettingsService.cs rename to src/EventLogExpert.UI/Settings/ISettingsService.cs index f699bdfb..473fb26a 100644 --- a/src/EventLogExpert.UI/Services/Settings/ISettingsService.cs +++ b/src/EventLogExpert.UI/Settings/ISettingsService.cs @@ -1,10 +1,10 @@ -// // 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 { diff --git a/src/EventLogExpert.UI/Services/Settings/SettingsService.cs b/src/EventLogExpert.UI/Settings/SettingsService.cs similarity index 97% rename from src/EventLogExpert.UI/Services/Settings/SettingsService.cs rename to src/EventLogExpert.UI/Settings/SettingsService.cs index 1d14ea69..91287fb6 100644 --- a/src/EventLogExpert.UI/Services/Settings/SettingsService.cs +++ b/src/EventLogExpert.UI/Settings/SettingsService.cs @@ -3,10 +3,9 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Preferences; -using EventLogExpert.UI.Interfaces; using Microsoft.Extensions.Logging; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Settings; public sealed class SettingsService(IPreferencesProvider preferences) : ISettingsService { diff --git a/src/EventLogExpert.UI/Enums/Theme.cs b/src/EventLogExpert.UI/Settings/Theme.cs similarity index 78% rename from src/EventLogExpert.UI/Enums/Theme.cs rename to src/EventLogExpert.UI/Settings/Theme.cs index 094b250d..515f6f1a 100644 --- a/src/EventLogExpert.UI/Enums/Theme.cs +++ b/src/EventLogExpert.UI/Settings/Theme.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Settings; public enum Theme { diff --git a/src/EventLogExpert/App.xaml.cs b/src/EventLogExpert/App.xaml.cs index 1750ef7d..ba7bfdcf 100644 --- a/src/EventLogExpert/App.xaml.cs +++ b/src/EventLogExpert/App.xaml.cs @@ -3,10 +3,9 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; -using EventLogExpert.UI; using EventLogExpert.UI.Common.AppTitle; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using Fluxor; using System.Collections.Immutable; diff --git a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs index 9f99c567..1bf67bb7 100644 --- a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs +++ b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs @@ -3,8 +3,8 @@ using EventLogExpert.Services; using EventLogExpert.UI.Common.AppTitle; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; +using EventLogExpert.UI.Settings; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs index e8b6c7b3..e053252b 100644 --- a/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/DetailsPane.razor.cs @@ -5,7 +5,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Common.Preferences; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using Fluxor; using Microsoft.AspNetCore.Components; diff --git a/src/EventLogExpert/Components/Sections/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index 805ddd02..1bc16c00 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -8,6 +8,7 @@ using EventLogExpert.UI.Common.Display; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.FilterPane; diff --git a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs index 703b9e48..7e362f1e 100644 --- a/src/EventLogExpert/Components/Sections/FilterPane.razor.cs +++ b/src/EventLogExpert/Components/Sections/FilterPane.razor.cs @@ -4,9 +4,9 @@ using EventLogExpert.Components.Modals.Filters; using EventLogExpert.UI; using EventLogExpert.UI.Common.Display; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Modal; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; using Fluxor; diff --git a/src/EventLogExpert/MainPage.xaml.cs b/src/EventLogExpert/MainPage.xaml.cs index c121f1c5..510e6cda 100644 --- a/src/EventLogExpert/MainPage.xaml.cs +++ b/src/EventLogExpert/MainPage.xaml.cs @@ -4,10 +4,9 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.Eventing.Logging; using EventLogExpert.Services; -using EventLogExpert.UI; using EventLogExpert.UI.Common.AppTitle; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using EventLogExpert.UI.Store.FilterCache; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 7d46c148..ed79fed9 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -21,6 +21,7 @@ using EventLogExpert.UI.Modal; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using Fluxor; diff --git a/src/EventLogExpert/Services/ClipboardService.cs b/src/EventLogExpert/Services/ClipboardService.cs index dd152db7..fcc53c54 100644 --- a/src/EventLogExpert/Services/ClipboardService.cs +++ b/src/EventLogExpert/Services/ClipboardService.cs @@ -7,7 +7,7 @@ using EventLogExpert.UI; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Display; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.EventTable; using Fluxor; diff --git a/src/EventLogExpert/Services/KeyboardShortcutService.cs b/src/EventLogExpert/Services/KeyboardShortcutService.cs index 17f21f02..6b99390b 100644 --- a/src/EventLogExpert/Services/KeyboardShortcutService.cs +++ b/src/EventLogExpert/Services/KeyboardShortcutService.cs @@ -3,6 +3,7 @@ using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Modal; +using EventLogExpert.UI.Settings; using Microsoft.JSInterop; namespace EventLogExpert.Services; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 4f1ef734..f695b7b4 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -14,6 +14,7 @@ using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Modal; using EventLogExpert.UI.Services; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; using Fluxor; diff --git a/src/EventLogExpert/Services/PreferencesProvider.cs b/src/EventLogExpert/Services/PreferencesProvider.cs index 5d3e9014..33e69b04 100644 --- a/src/EventLogExpert/Services/PreferencesProvider.cs +++ b/src/EventLogExpert/Services/PreferencesProvider.cs @@ -5,6 +5,7 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Models; +using EventLogExpert.UI.Settings; using Microsoft.Extensions.Logging; using System.Text.Json; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index fbd32442..bdc5b875 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -22,6 +22,7 @@ @using EventLogExpert.UI.Common.Display @using EventLogExpert.UI.Banner @using EventLogExpert.UI.Alerts +@using EventLogExpert.UI.Settings @using EventLogExpert.UI.Models @using Fluxor diff --git a/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs index 4689b66e..31b75262 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/DebugLog/DebugLogServiceTests.cs @@ -2,8 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.UI.DebugLog; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; using NSubstitute; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Settings/SettingsServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Settings/SettingsServiceTests.cs index fe3f2692..a575b79e 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Settings/SettingsServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Settings/SettingsServiceTests.cs @@ -3,12 +3,12 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Preferences; -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Settings; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Extensions.Logging; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services.Settings; +namespace EventLogExpert.UI.Tests.Settings; public sealed class SettingsServiceTests { From db1edf261ffd41b0e3e7176b958f4306ad0d4a1f Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 21:09:39 -0500 Subject: [PATCH 16/33] Promote Menu slice --- src/EventLogExpert.Components/BannerHost.razor.cs | 1 + src/EventLogExpert.Components/Menu/MenuBar.razor.cs | 3 +-- src/EventLogExpert.Components/Menu/MenuHost.razor.cs | 2 +- src/EventLogExpert.Components/Menu/MenuRenderer.razor.cs | 2 +- src/EventLogExpert.Components/_Imports.razor | 1 + .../{Interfaces => Menu}/IMenuActionService.cs | 2 +- src/EventLogExpert.UI/{Services => }/Menu/IMenuService.cs | 4 +--- src/EventLogExpert.UI/{Models => }/Menu/MenuItem.cs | 2 +- src/EventLogExpert.UI/{Services => }/Menu/MenuService.cs | 5 +---- src/EventLogExpert/Components/Sections/EventTable.razor.cs | 1 + src/EventLogExpert/GlobalUsings.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/Services/KeyboardShortcutService.cs | 2 +- src/EventLogExpert/Services/MauiMenuActionService.cs | 2 +- src/EventLogExpert/_Imports.razor | 1 + .../Unit/EventLogExpert.Components.Tests/BannerHostTests.cs | 1 + .../{Services => }/Menu/MenuServiceTests.cs | 5 ++--- 17 files changed, 18 insertions(+), 19 deletions(-) rename src/EventLogExpert.UI/{Interfaces => Menu}/IMenuActionService.cs (96%) rename src/EventLogExpert.UI/{Services => }/Menu/IMenuService.cs (96%) rename src/EventLogExpert.UI/{Models => }/Menu/MenuItem.cs (98%) rename src/EventLogExpert.UI/{Services => }/Menu/MenuService.cs (93%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Menu/MenuServiceTests.cs (97%) diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index b3077720..2afa230a 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -5,6 +5,7 @@ using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs index 87b95582..3bd93be5 100644 --- a/src/EventLogExpert.Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert.Components/Menu/MenuBar.razor.cs @@ -4,8 +4,7 @@ using EventLogExpert.Eventing.Common.Channels; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; 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 0f1bc015..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; diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index fb56d366..3a742835 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -13,6 +13,7 @@ @using EventLogExpert.UI.Banner @using EventLogExpert.UI.Alerts @using EventLogExpert.UI.Settings +@using EventLogExpert.UI.Menu @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs b/src/EventLogExpert.UI/Menu/IMenuActionService.cs similarity index 96% rename from src/EventLogExpert.UI/Interfaces/IMenuActionService.cs rename to src/EventLogExpert.UI/Menu/IMenuActionService.cs index 24a39840..7a59b6e7 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs +++ b/src/EventLogExpert.UI/Menu/IMenuActionService.cs @@ -3,7 +3,7 @@ using EventLogExpert.UI.Common.Clipboard; -namespace EventLogExpert.UI.Interfaces; +namespace EventLogExpert.UI.Menu; public interface IMenuActionService { diff --git a/src/EventLogExpert.UI/Services/Menu/IMenuService.cs b/src/EventLogExpert.UI/Menu/IMenuService.cs similarity index 96% rename from src/EventLogExpert.UI/Services/Menu/IMenuService.cs rename to src/EventLogExpert.UI/Menu/IMenuService.cs index 92d3b905..946670f4 100644 --- a/src/EventLogExpert.UI/Services/Menu/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/Menu/MenuItem.cs b/src/EventLogExpert.UI/Menu/MenuItem.cs similarity index 98% rename from src/EventLogExpert.UI/Models/Menu/MenuItem.cs rename to src/EventLogExpert.UI/Menu/MenuItem.cs index a0e7b338..b35c4048 100644 --- a/src/EventLogExpert.UI/Models/Menu/MenuItem.cs +++ b/src/EventLogExpert.UI/Menu/MenuItem.cs @@ -1,7 +1,7 @@ // // 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 diff --git a/src/EventLogExpert.UI/Services/Menu/MenuService.cs b/src/EventLogExpert.UI/Menu/MenuService.cs similarity index 93% rename from src/EventLogExpert.UI/Services/Menu/MenuService.cs rename to src/EventLogExpert.UI/Menu/MenuService.cs index 2ad9fe44..34a2ac6a 100644 --- a/src/EventLogExpert.UI/Services/Menu/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/Components/Sections/EventTable.razor.cs b/src/EventLogExpert/Components/Sections/EventTable.razor.cs index 1bc16c00..fd43b109 100644 --- a/src/EventLogExpert/Components/Sections/EventTable.razor.cs +++ b/src/EventLogExpert/Components/Sections/EventTable.razor.cs @@ -7,6 +7,7 @@ using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Display; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Models; using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; diff --git a/src/EventLogExpert/GlobalUsings.cs b/src/EventLogExpert/GlobalUsings.cs index 4b1419db..190965f5 100644 --- a/src/EventLogExpert/GlobalUsings.cs +++ b/src/EventLogExpert/GlobalUsings.cs @@ -3,4 +3,4 @@ // Disambiguates UI menu data records from Microsoft.Maui.Controls.MenuItem across all // Razor and code-behind files in the MAUI project. -global using MenuItem = EventLogExpert.UI.Models.MenuItem; +global using MenuItem = EventLogExpert.UI.Menu.MenuItem; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index ed79fed9..2011f45c 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -18,6 +18,7 @@ using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Modal; using EventLogExpert.UI.Options; using EventLogExpert.UI.Services; diff --git a/src/EventLogExpert/Services/KeyboardShortcutService.cs b/src/EventLogExpert/Services/KeyboardShortcutService.cs index 6b99390b..8ec38344 100644 --- a/src/EventLogExpert/Services/KeyboardShortcutService.cs +++ b/src/EventLogExpert/Services/KeyboardShortcutService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Modal; using EventLogExpert.UI.Settings; using Microsoft.JSInterop; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index f695b7b4..05fe5d57 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -11,7 +11,7 @@ using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Modal; using EventLogExpert.UI.Services; using EventLogExpert.UI.Settings; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index bdc5b875..3ba95d9d 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -23,6 +23,7 @@ @using EventLogExpert.UI.Banner @using EventLogExpert.UI.Alerts @using EventLogExpert.UI.Settings +@using EventLogExpert.UI.Menu @using EventLogExpert.UI.Models @using Fluxor diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs index 4a7de279..850bef4f 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -7,6 +7,7 @@ using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Menu; using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Menu/MenuServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Menu/MenuServiceTests.cs similarity index 97% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Menu/MenuServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Menu/MenuServiceTests.cs index af1d02dc..1cd91a9d 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Menu/MenuServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Menu/MenuServiceTests.cs @@ -1,10 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Menu; -namespace EventLogExpert.UI.Tests.Services.Menu; +namespace EventLogExpert.UI.Tests.Menu; public sealed class MenuServiceTests { From b7f93c5be5598e7974e8cb9d8d3404454bd11efa Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 21:20:04 -0500 Subject: [PATCH 17/33] Promote Database slice --- src/EventLogExpert.Components/BannerHost.razor.cs | 2 +- .../Database/DatabaseEntryRow.razor.cs | 3 +-- .../Database/DatabaseRecoveryDialog.razor.cs | 3 +-- .../Database/DatabaseRecoveryHost.razor.cs | 2 +- .../Modals/SettingsModal.razor.cs | 5 ++--- src/EventLogExpert.Components/_Imports.razor | 2 ++ src/EventLogExpert.UI/Banner/BannerProgressEntry.cs | 2 +- src/EventLogExpert.UI/Banner/BannerService.cs | 4 ++-- src/EventLogExpert.UI/Banner/BannerViewSelector.cs | 2 +- src/EventLogExpert.UI/Banner/IBannerService.cs | 2 +- .../{Models => }/Database/DatabaseEntry.cs | 2 +- .../{Services => }/Database/DatabaseService.cs | 5 ++--- .../{Enums => Database}/DatabaseStatus.cs | 2 +- .../{Defaults => Database}/DatabaseStatusLabels.cs | 4 +--- .../{Services => }/Database/IDatabaseService.cs | 6 +++--- .../{Models => }/Database/ImportFailure.cs | 2 +- .../{Models => }/Database/ImportResult.cs | 2 +- .../Upgrade/UpgradeBatchCompletedEventArgs.cs | 2 +- .../Upgrade/UpgradeBatchProgressEventArgs.cs | 2 +- .../{Models => Database}/Upgrade/UpgradeBatchResult.cs | 2 +- .../Upgrade/UpgradeBatchStartedEventArgs.cs | 2 +- .../{Models => Database}/Upgrade/UpgradeFailure.cs | 2 +- .../{Models => Database}/Upgrade/UpgradePhase.cs | 2 +- .../{Models => Database}/Upgrade/UpgradeProgressScope.cs | 2 +- src/EventLogExpert.UI/Store/EventLog/Effects.cs | 1 + src/EventLogExpert/MauiProgram.cs | 1 + src/EventLogExpert/_Imports.razor | 2 ++ .../EventLogExpert.Components.Tests/BannerHostTests.cs | 4 ++-- .../Database/DatabaseEntryRowTests.cs | 3 +-- .../Database/DatabaseRecoveryDialogTests.cs | 4 +--- .../Database/DatabaseRecoveryHostTests.cs | 4 +--- .../Database/SettingsUpgradeProgressBannerTests.cs | 2 +- .../EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs | 4 ++-- .../Banner/BannerViewSelectorTests.cs | 3 ++- .../{Services => }/Database/DatabaseServiceTests.cs | 7 +++---- .../{Defaults => Database}/DatabaseStatusLabelsTests.cs | 4 ++-- .../EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs | 1 + 37 files changed, 50 insertions(+), 54 deletions(-) rename src/EventLogExpert.UI/{Models => }/Database/DatabaseEntry.cs (86%) rename src/EventLogExpert.UI/{Services => }/Database/DatabaseService.cs (99%) rename src/EventLogExpert.UI/{Enums => Database}/DatabaseStatus.cs (86%) rename src/EventLogExpert.UI/{Defaults => Database}/DatabaseStatusLabels.cs (92%) rename src/EventLogExpert.UI/{Services => }/Database/IDatabaseService.cs (93%) rename src/EventLogExpert.UI/{Models => }/Database/ImportFailure.cs (79%) rename src/EventLogExpert.UI/{Models => }/Database/ImportResult.cs (85%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradeBatchCompletedEventArgs.cs (88%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradeBatchProgressEventArgs.cs (89%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradeBatchResult.cs (83%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradeBatchStartedEventArgs.cs (93%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradeFailure.cs (76%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradePhase.cs (77%) rename src/EventLogExpert.UI/{Models => Database}/Upgrade/UpgradeProgressScope.cs (77%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Database/DatabaseServiceTests.cs (99%) rename tests/Unit/EventLogExpert.UI.Tests/{Defaults => Database}/DatabaseStatusLabelsTests.cs (97%) diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index 2afa230a..743929a1 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -4,9 +4,9 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Database; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Menu; -using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs b/src/EventLogExpert.Components/Database/DatabaseEntryRow.razor.cs index 7cbd32db..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; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs index 198ab366..8cf5dc5a 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryDialog.razor.cs @@ -4,8 +4,7 @@ using EventLogExpert.Components.Base; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Database; diff --git a/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs b/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs index 51b36765..f8f4f914 100644 --- a/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs +++ b/src/EventLogExpert.Components/Database/DatabaseRecoveryHost.razor.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Database; using Microsoft.AspNetCore.Components; namespace EventLogExpert.Components.Database; diff --git a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs index 24edc1e1..a09bb67a 100644 --- a/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs +++ b/src/EventLogExpert.Components/Modals/SettingsModal.razor.cs @@ -2,13 +2,12 @@ // // Licensed under the MIT License. using EventLogExpert.Components.Base; -using EventLogExpert.UI; using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; using EventLogExpert.UI.Common.Files; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using Fluxor; diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 3a742835..9e1fa1c1 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -14,6 +14,8 @@ @using EventLogExpert.UI.Alerts @using EventLogExpert.UI.Settings @using EventLogExpert.UI.Menu +@using EventLogExpert.UI.Database +@using EventLogExpert.UI.Database.Upgrade @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs b/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs index 75537956..62e549fb 100644 --- a/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs +++ b/src/EventLogExpert.UI/Banner/BannerProgressEntry.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database.Upgrade; namespace EventLogExpert.UI.Banner; diff --git a/src/EventLogExpert.UI/Banner/BannerService.cs b/src/EventLogExpert.UI/Banner/BannerService.cs index ed9824bd..ba01e21c 100644 --- a/src/EventLogExpert.UI/Banner/BannerService.cs +++ b/src/EventLogExpert.UI/Banner/BannerService.cs @@ -2,8 +2,8 @@ // // 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.Banner; diff --git a/src/EventLogExpert.UI/Banner/BannerViewSelector.cs b/src/EventLogExpert.UI/Banner/BannerViewSelector.cs index 69ce8d08..08d241ed 100644 --- a/src/EventLogExpert.UI/Banner/BannerViewSelector.cs +++ b/src/EventLogExpert.UI/Banner/BannerViewSelector.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; namespace EventLogExpert.UI.Banner; diff --git a/src/EventLogExpert.UI/Banner/IBannerService.cs b/src/EventLogExpert.UI/Banner/IBannerService.cs index fd871d75..1b00fe8e 100644 --- a/src/EventLogExpert.UI/Banner/IBannerService.cs +++ b/src/EventLogExpert.UI/Banner/IBannerService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; namespace EventLogExpert.UI.Banner; diff --git a/src/EventLogExpert.UI/Models/Database/DatabaseEntry.cs b/src/EventLogExpert.UI/Database/DatabaseEntry.cs similarity index 86% rename from src/EventLogExpert.UI/Models/Database/DatabaseEntry.cs rename to src/EventLogExpert.UI/Database/DatabaseEntry.cs index 5f923961..68d10ba1 100644 --- a/src/EventLogExpert.UI/Models/Database/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/Database/DatabaseService.cs b/src/EventLogExpert.UI/Database/DatabaseService.cs similarity index 99% rename from src/EventLogExpert.UI/Services/Database/DatabaseService.cs rename to src/EventLogExpert.UI/Database/DatabaseService.cs index c257c202..43c049c9 100644 --- a/src/EventLogExpert.UI/Services/Database/DatabaseService.cs +++ b/src/EventLogExpert.UI/Database/DatabaseService.cs @@ -5,15 +5,14 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Eventing.ProviderDatabase; using EventLogExpert.UI.Common.Preferences; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database.Upgrade; using EventLogExpert.UI.Options; 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 { diff --git a/src/EventLogExpert.UI/Enums/DatabaseStatus.cs b/src/EventLogExpert.UI/Database/DatabaseStatus.cs similarity index 86% rename from src/EventLogExpert.UI/Enums/DatabaseStatus.cs rename to src/EventLogExpert.UI/Database/DatabaseStatus.cs index e77802cd..bb87580a 100644 --- a/src/EventLogExpert.UI/Enums/DatabaseStatus.cs +++ b/src/EventLogExpert.UI/Database/DatabaseStatus.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI; +namespace EventLogExpert.UI.Database; public enum DatabaseStatus { diff --git a/src/EventLogExpert.UI/Defaults/DatabaseStatusLabels.cs b/src/EventLogExpert.UI/Database/DatabaseStatusLabels.cs similarity index 92% rename from src/EventLogExpert.UI/Defaults/DatabaseStatusLabels.cs rename to src/EventLogExpert.UI/Database/DatabaseStatusLabels.cs index 12a9f59b..9c21ac70 100644 --- a/src/EventLogExpert.UI/Defaults/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/Services/Database/IDatabaseService.cs b/src/EventLogExpert.UI/Database/IDatabaseService.cs similarity index 93% rename from src/EventLogExpert.UI/Services/Database/IDatabaseService.cs rename to src/EventLogExpert.UI/Database/IDatabaseService.cs index e745ea0d..bf9b143b 100644 --- a/src/EventLogExpert.UI/Services/Database/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/Database/ImportFailure.cs b/src/EventLogExpert.UI/Database/ImportFailure.cs similarity index 79% rename from src/EventLogExpert.UI/Models/Database/ImportFailure.cs rename to src/EventLogExpert.UI/Database/ImportFailure.cs index 3521461a..c04facfb 100644 --- a/src/EventLogExpert.UI/Models/Database/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/Database/ImportResult.cs b/src/EventLogExpert.UI/Database/ImportResult.cs similarity index 85% rename from src/EventLogExpert.UI/Models/Database/ImportResult.cs rename to src/EventLogExpert.UI/Database/ImportResult.cs index bdcf8ec8..18a5acdb 100644 --- a/src/EventLogExpert.UI/Models/Database/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/Models/Upgrade/UpgradeBatchCompletedEventArgs.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchCompletedEventArgs.cs similarity index 88% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchCompletedEventArgs.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchCompletedEventArgs.cs index 469dab0e..a458462d 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchCompletedEventArgs.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchCompletedEventArgs.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 class UpgradeBatchCompletedEventArgs(Guid batchId, UpgradeBatchResult result, bool wasCancelled) : EventArgs { diff --git a/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchProgressEventArgs.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchProgressEventArgs.cs similarity index 89% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchProgressEventArgs.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchProgressEventArgs.cs index b6d8452e..3133b94b 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchProgressEventArgs.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchProgressEventArgs.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 class UpgradeBatchProgressEventArgs(Guid batchId, int position, string fileName, UpgradePhase phase) : EventArgs { diff --git a/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchResult.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchResult.cs similarity index 83% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchResult.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchResult.cs index 9dd5c51f..2538c25e 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/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/Upgrade/UpgradeBatchStartedEventArgs.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchStartedEventArgs.cs similarity index 93% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchStartedEventArgs.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchStartedEventArgs.cs index 721a86c6..b9541fe9 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/UpgradeBatchStartedEventArgs.cs +++ b/src/EventLogExpert.UI/Database/Upgrade/UpgradeBatchStartedEventArgs.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 class UpgradeBatchStartedEventArgs( Guid batchId, diff --git a/src/EventLogExpert.UI/Models/Upgrade/UpgradeFailure.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeFailure.cs similarity index 76% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradeFailure.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeFailure.cs index 76b94aa0..de3ab1dd 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/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/Upgrade/UpgradePhase.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradePhase.cs similarity index 77% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradePhase.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradePhase.cs index 36ffb9cd..1d901d3a 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/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/Upgrade/UpgradeProgressScope.cs b/src/EventLogExpert.UI/Database/Upgrade/UpgradeProgressScope.cs similarity index 77% rename from src/EventLogExpert.UI/Models/Upgrade/UpgradeProgressScope.cs rename to src/EventLogExpert.UI/Database/Upgrade/UpgradeProgressScope.cs index d7dbade3..f026300e 100644 --- a/src/EventLogExpert.UI/Models/Upgrade/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/Store/EventLog/Effects.cs b/src/EventLogExpert.UI/Store/EventLog/Effects.cs index 4e0899f9..97447945 100644 --- a/src/EventLogExpert.UI/Store/EventLog/Effects.cs +++ b/src/EventLogExpert.UI/Store/EventLog/Effects.cs @@ -7,6 +7,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Banner; +using EventLogExpert.UI.Database; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.StatusBar; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 2011f45c..16a56204 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -16,6 +16,7 @@ using EventLogExpert.UI.Common.Preferences; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Common.Versioning; +using EventLogExpert.UI.Database; using EventLogExpert.UI.DebugLog; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Menu; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index 3ba95d9d..4643b910 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -24,6 +24,8 @@ @using EventLogExpert.UI.Alerts @using EventLogExpert.UI.Settings @using EventLogExpert.UI.Menu +@using EventLogExpert.UI.Database +@using EventLogExpert.UI.Database.Upgrade @using EventLogExpert.UI.Models @using Fluxor diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs index 850bef4f..9bc29215 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -3,12 +3,12 @@ using Bunit; using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Menu; -using EventLogExpert.UI.Models; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using Microsoft.JSInterop; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs index 120396a2..a921a645 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseEntryRowTests.cs @@ -3,8 +3,7 @@ using Bunit; using EventLogExpert.Components.Database; -using EventLogExpert.UI; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs index f9b37ecb..640ab1d6 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryDialogTests.cs @@ -6,10 +6,8 @@ using Bunit; using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI; using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs index 5042909f..40da6c78 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/DatabaseRecoveryHostTests.cs @@ -4,10 +4,8 @@ using Bunit; using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI; using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using NSubstitute; diff --git a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs index 6c991798..51f36571 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/Database/SettingsUpgradeProgressBannerTests.cs @@ -5,7 +5,7 @@ using EventLogExpert.Components.Database; using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database.Upgrade; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; using NSubstitute; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs index 46b758fa..60d20003 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerServiceTests.cs @@ -3,8 +3,8 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; using NSubstitute; namespace EventLogExpert.UI.Tests.Banner; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs index c38d5cbc..171b315d 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Banner/BannerViewSelectorTests.cs @@ -2,7 +2,8 @@ // // Licensed under the MIT License. using EventLogExpert.UI.Banner; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; namespace EventLogExpert.UI.Tests.Banner; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseServiceTests.cs index 1d44daad..965bd71a 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Database/DatabaseServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseServiceTests.cs @@ -3,17 +3,16 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Common.Preferences; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Database; +using EventLogExpert.UI.Database.Upgrade; using EventLogExpert.UI.Options; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; using Microsoft.Data.Sqlite; using NSubstitute; using System.IO.Compression; -namespace EventLogExpert.UI.Tests.Services.Database; +namespace EventLogExpert.UI.Tests.Database; public sealed class DatabaseServiceTests : IDisposable { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Defaults/DatabaseStatusLabelsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseStatusLabelsTests.cs similarity index 97% rename from tests/Unit/EventLogExpert.UI.Tests/Defaults/DatabaseStatusLabelsTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseStatusLabelsTests.cs index 209db6b5..0ffdbd79 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Defaults/DatabaseStatusLabelsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Database/DatabaseStatusLabelsTests.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.Tests.Defaults; +namespace EventLogExpert.UI.Tests.Database; public sealed class DatabaseStatusLabelsTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs index 4dc77cf8..14a5343b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Store/EventLog/EffectsTests.cs @@ -7,6 +7,7 @@ using EventLogExpert.Eventing.Readers; using EventLogExpert.Eventing.Resolvers; using EventLogExpert.UI.Banner; +using EventLogExpert.UI.Database; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Models; using EventLogExpert.UI.StatusBar; From 4af675d7972fd7f5f5c244a69e902473f7d9c7b4 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 21:51:17 -0500 Subject: [PATCH 18/33] Promote Update slice and move ApplicationRestart port to Common/ --- src/EventLogExpert.Components/BannerHost.razor.cs | 2 +- src/EventLogExpert.Components/_Imports.razor | 1 + .../System => Common/Restart}/IApplicationRestartService.cs | 2 +- .../Services/Deployment/DeploymentService.cs | 1 + src/EventLogExpert.UI/{Services => }/Update/GitHubService.cs | 3 +-- src/EventLogExpert.UI/{Models => }/Update/GitReleaseModel.cs | 4 ++-- .../{Services => }/Update/IGitHubService.cs | 4 +--- .../{Services => }/Update/IUpdateService.cs | 4 +++- src/EventLogExpert.UI/{Services => }/Update/UpdateService.cs | 4 ++-- src/EventLogExpert/Components/Layout/MainLayout.razor.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 2 ++ src/EventLogExpert/Services/MauiMenuActionService.cs | 2 +- .../Services/WindowsApplicationRestartService.cs | 2 +- src/EventLogExpert/_Imports.razor | 1 + .../Unit/EventLogExpert.Components.Tests/BannerHostTests.cs | 2 +- .../Services/Deployment/DeploymentServiceTests.cs | 1 + tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs | 4 ++-- .../{Services => }/Update/GitHubServiceTests.cs | 5 ++--- .../{Services => }/Update/UpdateServiceTests.cs | 5 ++--- 19 files changed, 27 insertions(+), 24 deletions(-) rename src/EventLogExpert.UI/{Services/System => Common/Restart}/IApplicationRestartService.cs (89%) rename src/EventLogExpert.UI/{Services => }/Update/GitHubService.cs (95%) rename src/EventLogExpert.UI/{Models => }/Update/GitReleaseModel.cs (95%) rename src/EventLogExpert.UI/{Services => }/Update/IGitHubService.cs (70%) rename src/EventLogExpert.UI/{Services => }/Update/IUpdateService.cs (77%) rename src/EventLogExpert.UI/{Services => }/Update/UpdateService.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Update/GitHubServiceTests.cs (98%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => }/Update/UpdateServiceTests.cs (99%) diff --git a/src/EventLogExpert.Components/BannerHost.razor.cs b/src/EventLogExpert.Components/BannerHost.razor.cs index 743929a1..4ad5ffc3 100644 --- a/src/EventLogExpert.Components/BannerHost.razor.cs +++ b/src/EventLogExpert.Components/BannerHost.razor.cs @@ -4,8 +4,8 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Database; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Menu; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert.Components/_Imports.razor b/src/EventLogExpert.Components/_Imports.razor index 9e1fa1c1..151c4431 100644 --- a/src/EventLogExpert.Components/_Imports.razor +++ b/src/EventLogExpert.Components/_Imports.razor @@ -16,6 +16,7 @@ @using EventLogExpert.UI.Menu @using EventLogExpert.UI.Database @using EventLogExpert.UI.Database.Upgrade +@using EventLogExpert.UI.Update @using EventLogExpert.UI.Models @using EventLogExpert.UI.Services @using Fluxor diff --git a/src/EventLogExpert.UI/Services/System/IApplicationRestartService.cs b/src/EventLogExpert.UI/Common/Restart/IApplicationRestartService.cs similarity index 89% rename from src/EventLogExpert.UI/Services/System/IApplicationRestartService.cs rename to src/EventLogExpert.UI/Common/Restart/IApplicationRestartService.cs index 98da40e6..6320318b 100644 --- a/src/EventLogExpert.UI/Services/System/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/Services/Deployment/DeploymentService.cs b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs index 2c1c9570..6335f2be 100644 --- a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs +++ b/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; diff --git a/src/EventLogExpert.UI/Services/Update/GitHubService.cs b/src/EventLogExpert.UI/Update/GitHubService.cs similarity index 95% rename from src/EventLogExpert.UI/Services/Update/GitHubService.cs rename to src/EventLogExpert.UI/Update/GitHubService.cs index b10ca794..ac4909dc 100644 --- a/src/EventLogExpert.UI/Services/Update/GitHubService.cs +++ b/src/EventLogExpert.UI/Update/GitHubService.cs @@ -2,10 +2,9 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Models; using System.Text.Json; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update; public sealed class GitHubService(HttpClient httpClient, ITraceLogger traceLogger) : IGitHubService { diff --git a/src/EventLogExpert.UI/Models/Update/GitReleaseModel.cs b/src/EventLogExpert.UI/Update/GitReleaseModel.cs similarity index 95% rename from src/EventLogExpert.UI/Models/Update/GitReleaseModel.cs rename to src/EventLogExpert.UI/Update/GitReleaseModel.cs index 1032e7f4..96b8d3ee 100644 --- a/src/EventLogExpert.UI/Models/Update/GitReleaseModel.cs +++ b/src/EventLogExpert.UI/Update/GitReleaseModel.cs @@ -1,10 +1,10 @@ -// // 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 { diff --git a/src/EventLogExpert.UI/Services/Update/IGitHubService.cs b/src/EventLogExpert.UI/Update/IGitHubService.cs similarity index 70% rename from src/EventLogExpert.UI/Services/Update/IGitHubService.cs rename to src/EventLogExpert.UI/Update/IGitHubService.cs index 4603aa19..1ac33738 100644 --- a/src/EventLogExpert.UI/Services/Update/IGitHubService.cs +++ b/src/EventLogExpert.UI/Update/IGitHubService.cs @@ -1,9 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Models; - -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update; public interface IGitHubService { diff --git a/src/EventLogExpert.UI/Services/Update/IUpdateService.cs b/src/EventLogExpert.UI/Update/IUpdateService.cs similarity index 77% rename from src/EventLogExpert.UI/Services/Update/IUpdateService.cs rename to src/EventLogExpert.UI/Update/IUpdateService.cs index 42bcce99..e263e0fb 100644 --- a/src/EventLogExpert.UI/Services/Update/IUpdateService.cs +++ b/src/EventLogExpert.UI/Update/IUpdateService.cs @@ -1,7 +1,9 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Services; +using EventLogExpert.UI.Services; + +namespace EventLogExpert.UI.Update; public interface IUpdateService { diff --git a/src/EventLogExpert.UI/Services/Update/UpdateService.cs b/src/EventLogExpert.UI/Update/UpdateService.cs similarity index 98% rename from src/EventLogExpert.UI/Services/Update/UpdateService.cs rename to src/EventLogExpert.UI/Update/UpdateService.cs index 1144f63e..d61e86ba 100644 --- a/src/EventLogExpert.UI/Services/Update/UpdateService.cs +++ b/src/EventLogExpert.UI/Update/UpdateService.cs @@ -6,9 +6,9 @@ using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; +using EventLogExpert.UI.Services; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update; public sealed class UpdateService( ICurrentVersionProvider versionProvider, diff --git a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs index 1bf67bb7..428a93e4 100644 --- a/src/EventLogExpert/Components/Layout/MainLayout.razor.cs +++ b/src/EventLogExpert/Components/Layout/MainLayout.razor.cs @@ -3,8 +3,8 @@ using EventLogExpert.Services; using EventLogExpert.UI.Common.AppTitle; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Settings; +using EventLogExpert.UI.Update; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index 16a56204..a0f94a8d 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -14,6 +14,7 @@ using EventLogExpert.UI.Common.Identity; using EventLogExpert.UI.Common.Logging; using EventLogExpert.UI.Common.Preferences; +using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Database; @@ -25,6 +26,7 @@ using EventLogExpert.UI.Services; using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; +using EventLogExpert.UI.Update; using Fluxor; namespace EventLogExpert; diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 05fe5d57..25194437 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -13,10 +13,10 @@ using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Menu; using EventLogExpert.UI.Modal; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; +using EventLogExpert.UI.Update; using Fluxor; using Microsoft.AspNetCore.Components; using Application = Microsoft.Maui.Controls.Application; diff --git a/src/EventLogExpert/Services/WindowsApplicationRestartService.cs b/src/EventLogExpert/Services/WindowsApplicationRestartService.cs index 82e7623a..3a0e04b5 100644 --- a/src/EventLogExpert/Services/WindowsApplicationRestartService.cs +++ b/src/EventLogExpert/Services/WindowsApplicationRestartService.cs @@ -3,7 +3,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.Interop; -using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Common.Restart; using Windows.ApplicationModel.Core; namespace EventLogExpert.Services; diff --git a/src/EventLogExpert/_Imports.razor b/src/EventLogExpert/_Imports.razor index 4643b910..dc936f06 100644 --- a/src/EventLogExpert/_Imports.razor +++ b/src/EventLogExpert/_Imports.razor @@ -26,6 +26,7 @@ @using EventLogExpert.UI.Menu @using EventLogExpert.UI.Database @using EventLogExpert.UI.Database.Upgrade +@using EventLogExpert.UI.Update @using EventLogExpert.UI.Models @using Fluxor diff --git a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs index 9bc29215..440ca6ed 100644 --- a/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs +++ b/tests/Unit/EventLogExpert.Components.Tests/BannerHostTests.cs @@ -5,9 +5,9 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Banner; using EventLogExpert.UI.Common.Clipboard; +using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Database; using EventLogExpert.UI.Database.Upgrade; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Menu; using Microsoft.AspNetCore.Components.Web; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs index c44747a9..b623405a 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs @@ -4,6 +4,7 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Common.Threading; using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Options; diff --git a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs index 6436aa15..8783863b 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/TestUtils/GitHubUtils.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.Update; using static EventLogExpert.UI.Tests.TestUtils.Constants.Constants; namespace EventLogExpert.UI.Tests.TestUtils; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/GitHubServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Update/GitHubServiceTests.cs similarity index 98% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Update/GitHubServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Update/GitHubServiceTests.cs index b40a1a02..7c3bb2ee 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/GitHubServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Update/GitHubServiceTests.cs @@ -2,14 +2,13 @@ // // Licensed under the MIT License. using EventLogExpert.Eventing.Logging; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; +using EventLogExpert.UI.Update; using NSubstitute; using System.Net; using System.Text.Json; -namespace EventLogExpert.UI.Tests.Services.Update; +namespace EventLogExpert.UI.Tests.Update; public sealed class GitHubServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs index 02a2c6b2..984a9131 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Update/UpdateServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs @@ -6,13 +6,12 @@ using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Models; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; +using EventLogExpert.UI.Update; using NSubstitute; -namespace EventLogExpert.UI.Tests.Services.Update; +namespace EventLogExpert.UI.Tests.Update; public sealed class UpdateServiceTests { From f9281004d32ad14029636129926e0b4818d71c5b Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 21:58:39 -0500 Subject: [PATCH 19/33] Promote Deployment subfolder under Update/ --- .../{Services => Update}/Deployment/DeploymentService.cs | 4 +--- .../{Services => Update}/Deployment/IDeploymentService.cs | 4 ++-- .../Deployment/IPackageDeploymentService.cs | 3 +-- .../Deployment}/PackageDeploymentOptions.cs | 2 +- .../Deployment/PackageDeploymentService.cs | 4 +--- src/EventLogExpert.UI/Update/UpdateService.cs | 2 +- src/EventLogExpert/MauiProgram.cs | 1 + .../Deployment/DeploymentServiceTests.cs | 6 ++---- .../EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs | 2 +- 9 files changed, 11 insertions(+), 17 deletions(-) rename src/EventLogExpert.UI/{Services => Update}/Deployment/DeploymentService.cs (97%) rename src/EventLogExpert.UI/{Services => Update}/Deployment/IDeploymentService.cs (72%) rename src/EventLogExpert.UI/{Services => Update}/Deployment/IPackageDeploymentService.cs (82%) rename src/EventLogExpert.UI/{Options => Update/Deployment}/PackageDeploymentOptions.cs (94%) rename src/EventLogExpert.UI/{Services => Update}/Deployment/PackageDeploymentService.cs (88%) rename tests/Unit/EventLogExpert.UI.Tests/{Services => Update}/Deployment/DeploymentServiceTests.cs (99%) diff --git a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/DeploymentService.cs similarity index 97% rename from src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/DeploymentService.cs index 6335f2be..89a195e2 100644 --- a/src/EventLogExpert.UI/Services/Deployment/DeploymentService.cs +++ b/src/EventLogExpert.UI/Update/Deployment/DeploymentService.cs @@ -6,13 +6,11 @@ using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Common.Threading; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Options; using System.Reflection; using Windows.Foundation; using Windows.Management.Deployment; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update.Deployment; public sealed class DeploymentService( ITraceLogger traceLogger, diff --git a/src/EventLogExpert.UI/Services/Deployment/IDeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/IDeploymentService.cs similarity index 72% rename from src/EventLogExpert.UI/Services/Deployment/IDeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/IDeploymentService.cs index 08dbfb8b..885101f0 100644 --- a/src/EventLogExpert.UI/Services/Deployment/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/Services/Deployment/IPackageDeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/IPackageDeploymentService.cs similarity index 82% rename from src/EventLogExpert.UI/Services/Deployment/IPackageDeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/IPackageDeploymentService.cs index b77753b7..4359da3b 100644 --- a/src/EventLogExpert.UI/Services/Deployment/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 94% rename from src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs rename to src/EventLogExpert.UI/Update/Deployment/PackageDeploymentOptions.cs index 85c66201..53fdccf4 100644 --- a/src/EventLogExpert.UI/Options/PackageDeploymentOptions.cs +++ b/src/EventLogExpert.UI/Update/Deployment/PackageDeploymentOptions.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Options; +namespace EventLogExpert.UI.Update.Deployment; /// Options for package deployment operations. /// diff --git a/src/EventLogExpert.UI/Services/Deployment/PackageDeploymentService.cs b/src/EventLogExpert.UI/Update/Deployment/PackageDeploymentService.cs similarity index 88% rename from src/EventLogExpert.UI/Services/Deployment/PackageDeploymentService.cs rename to src/EventLogExpert.UI/Update/Deployment/PackageDeploymentService.cs index fae7ad0f..f279568a 100644 --- a/src/EventLogExpert.UI/Services/Deployment/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/Update/UpdateService.cs b/src/EventLogExpert.UI/Update/UpdateService.cs index d61e86ba..bf14a779 100644 --- a/src/EventLogExpert.UI/Update/UpdateService.cs +++ b/src/EventLogExpert.UI/Update/UpdateService.cs @@ -5,8 +5,8 @@ using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Services; +using EventLogExpert.UI.Update.Deployment; namespace EventLogExpert.UI.Update; diff --git a/src/EventLogExpert/MauiProgram.cs b/src/EventLogExpert/MauiProgram.cs index a0f94a8d..96c1e752 100644 --- a/src/EventLogExpert/MauiProgram.cs +++ b/src/EventLogExpert/MauiProgram.cs @@ -27,6 +27,7 @@ using EventLogExpert.UI.Settings; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Update; +using EventLogExpert.UI.Update.Deployment; using Fluxor; namespace EventLogExpert; diff --git a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Update/Deployment/DeploymentServiceTests.cs similarity index 99% rename from tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Update/Deployment/DeploymentServiceTests.cs index b623405a..70b2da3c 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/Deployment/DeploymentServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Update/Deployment/DeploymentServiceTests.cs @@ -6,15 +6,13 @@ using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Restart; using EventLogExpert.UI.Common.Threading; -using EventLogExpert.UI.Interfaces; -using EventLogExpert.UI.Options; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; +using EventLogExpert.UI.Update.Deployment; using NSubstitute; using Windows.Foundation; -namespace EventLogExpert.UI.Tests.Services.Deployment; +namespace EventLogExpert.UI.Tests.Update.Deployment; public sealed class DeploymentServiceTests { diff --git a/tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs index 984a9131..60d032d9 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Update/UpdateServiceTests.cs @@ -5,10 +5,10 @@ using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Interfaces; using EventLogExpert.UI.Tests.TestUtils; using EventLogExpert.UI.Tests.TestUtils.Constants; using EventLogExpert.UI.Update; +using EventLogExpert.UI.Update.Deployment; using NSubstitute; namespace EventLogExpert.UI.Tests.Update; From 05168bca3ac18d93e10091f653b5e3736896b4b7 Mon Sep 17 00:00:00 2001 From: jschick04 Date: Sun, 10 May 2026 22:14:44 -0500 Subject: [PATCH 20/33] Promote ReleaseNotes subfolder; relocate Markdown helpers to Common/ --- .../Modals/ReleaseNotesModal.razor.cs | 5 +- .../Markdown/GitHubReleaseNormalizer.cs} | 12 +-- .../Markdown/MarkdownRenderer.cs} | 4 +- .../Update/IUpdateService.cs | 2 +- .../ReleaseNotes}/ReleaseNotesContent.cs | 2 +- src/EventLogExpert.UI/Update/UpdateService.cs | 5 +- .../Markdown/GitHubReleaseNormalizerTests.cs} | 34 +++---- .../Markdown/MarkdownRendererTests.cs} | 90 +++++++++---------- 8 files changed, 78 insertions(+), 76 deletions(-) rename src/EventLogExpert.UI/{Services/ReleaseNotes/ReleaseNotesNormalizer.cs => Common/Markdown/GitHubReleaseNormalizer.cs} (85%) rename src/EventLogExpert.UI/{Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs => Common/Markdown/MarkdownRenderer.cs} (98%) rename src/EventLogExpert.UI/{Services/Update => Update/ReleaseNotes}/ReleaseNotesContent.cs (77%) rename tests/Unit/EventLogExpert.UI.Tests/{Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs => Common/Markdown/GitHubReleaseNormalizerTests.cs} (73%) rename tests/Unit/EventLogExpert.UI.Tests/{Services/ReleaseNotes/ReleaseNotesMarkdownRendererTests.cs => Common/Markdown/MarkdownRendererTests.cs} (69%) 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.UI/Services/ReleaseNotes/ReleaseNotesNormalizer.cs b/src/EventLogExpert.UI/Common/Markdown/GitHubReleaseNormalizer.cs similarity index 85% rename from src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesNormalizer.cs rename to src/EventLogExpert.UI/Common/Markdown/GitHubReleaseNormalizer.cs index dddcc9bc..ba42ca38 100644 --- a/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesNormalizer.cs +++ b/src/EventLogExpert.UI/Common/Markdown/GitHubReleaseNormalizer.cs @@ -3,18 +3,18 @@ using System.Text.RegularExpressions; -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Common.Markdown; /// /// 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>/ +/// . 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. /// -internal static partial class ReleaseNotesNormalizer +internal static partial class GitHubReleaseNormalizer { public static string Normalize(string? rawBody) { diff --git a/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs b/src/EventLogExpert.UI/Common/Markdown/MarkdownRenderer.cs similarity index 98% rename from src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs rename to src/EventLogExpert.UI/Common/Markdown/MarkdownRenderer.cs index 5d2e9373..26b1c41a 100644 --- a/src/EventLogExpert.UI/Services/ReleaseNotes/ReleaseNotesMarkdownRenderer.cs +++ b/src/EventLogExpert.UI/Common/Markdown/MarkdownRenderer.cs @@ -6,13 +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. /// -public static partial class ReleaseNotesMarkdownRenderer +public static partial class MarkdownRenderer { private const char CodePlaceholderSentinel = '\u0001'; diff --git a/src/EventLogExpert.UI/Update/IUpdateService.cs b/src/EventLogExpert.UI/Update/IUpdateService.cs index e263e0fb..8e3dd1e9 100644 --- a/src/EventLogExpert.UI/Update/IUpdateService.cs +++ b/src/EventLogExpert.UI/Update/IUpdateService.cs @@ -1,7 +1,7 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Update.ReleaseNotes; namespace EventLogExpert.UI.Update; diff --git a/src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs b/src/EventLogExpert.UI/Update/ReleaseNotes/ReleaseNotesContent.cs similarity index 77% rename from src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs rename to src/EventLogExpert.UI/Update/ReleaseNotes/ReleaseNotesContent.cs index 139f6943..70c3dfdc 100644 --- a/src/EventLogExpert.UI/Services/Update/ReleaseNotesContent.cs +++ b/src/EventLogExpert.UI/Update/ReleaseNotes/ReleaseNotesContent.cs @@ -1,6 +1,6 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -namespace EventLogExpert.UI.Services; +namespace EventLogExpert.UI.Update.ReleaseNotes; public readonly record struct ReleaseNotesContent(string Title, string Markdown); diff --git a/src/EventLogExpert.UI/Update/UpdateService.cs b/src/EventLogExpert.UI/Update/UpdateService.cs index bf14a779..3deb5ab2 100644 --- a/src/EventLogExpert.UI/Update/UpdateService.cs +++ b/src/EventLogExpert.UI/Update/UpdateService.cs @@ -4,9 +4,10 @@ using EventLogExpert.Eventing.Logging; using EventLogExpert.UI.Alerts; using EventLogExpert.UI.Common.AppTitle; +using EventLogExpert.UI.Common.Markdown; using EventLogExpert.UI.Common.Versioning; -using EventLogExpert.UI.Services; using EventLogExpert.UI.Update.Deployment; +using EventLogExpert.UI.Update.ReleaseNotes; namespace EventLogExpert.UI.Update; @@ -172,7 +173,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/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs b/tests/Unit/EventLogExpert.UI.Tests/Common/Markdown/GitHubReleaseNormalizerTests.cs similarity index 73% rename from tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs rename to tests/Unit/EventLogExpert.UI.Tests/Common/Markdown/GitHubReleaseNormalizerTests.cs index 988f1ea4..8cace071 100644 --- a/tests/Unit/EventLogExpert.UI.Tests/Services/ReleaseNotes/ReleaseNotesNormalizerTests.cs +++ b/tests/Unit/EventLogExpert.UI.Tests/Common/Markdown/GitHubReleaseNormalizerTests.cs @@ -1,19 +1,19 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. -using EventLogExpert.UI.Services; +using EventLogExpert.UI.Common.Markdown; using EventLogExpert.UI.Tests.TestUtils.Constants; -namespace EventLogExpert.UI.Tests.Services.ReleaseNotes; +namespace EventLogExpert.UI.Tests.Common.Markdown; -public sealed class ReleaseNotesNormalizerTests +public sealed class GitHubReleaseNormalizerTests { [Fact] public void Normalize_BulletWithoutCommitPrefix_ConvertsToDashStyle() { const string raw = "* Just a description with no commit id"; - var result = ReleaseNotesNormalizer.Normalize(raw); + var result = GitHubReleaseNormalizer.Normalize(raw); Assert.Equal("- Just a description with no commit id", result); } @@ -21,7 +21,7 @@ public void Normalize_BulletWithoutCommitPrefix_ConvertsToDashStyle() [Fact] public void Normalize_EmptySummary_RemovedWithoutEmittingHeading() { - var result = ReleaseNotesNormalizer.Normalize("beforeafter"); + var result = GitHubReleaseNormalizer.Normalize("beforeafter"); Assert.DoesNotContain("##", result); Assert.Contains("before", result); @@ -31,7 +31,7 @@ public void Normalize_EmptySummary_RemovedWithoutEmittingHeading() [Fact] public void Normalize_LegacyFixture_ProducesCleanBullets() { - var result = ReleaseNotesNormalizer.Normalize(Constants.GitHubReleaseNotes); + var result = GitHubReleaseNormalizer.Normalize(Constants.GitHubReleaseNotes); Assert.Contains("## Changes:", result); Assert.Contains("- Fixed LF issue in App.xaml and added custom width for ultrawide monitors", result); @@ -43,7 +43,7 @@ public void Normalize_LegacyFixture_ProducesCleanBullets() [Fact] public void Normalize_LegacyFixture_PromotesSummaryTextToHeading() { - var result = ReleaseNotesNormalizer.Normalize(Constants.GitHubReleaseNotes); + var result = GitHubReleaseNormalizer.Normalize(Constants.GitHubReleaseNotes); Assert.Contains("## See More", result); Assert.Contains("- Reduce unnecessary sorting", result); @@ -52,7 +52,7 @@ public void Normalize_LegacyFixture_PromotesSummaryTextToHeading() [Fact] public void Normalize_LegacyFixture_StripsAutoGeneratedFooter() { - var result = ReleaseNotesNormalizer.Normalize(Constants.GitHubReleaseNotes); + var result = GitHubReleaseNormalizer.Normalize(Constants.GitHubReleaseNotes); Assert.DoesNotContain("This list of changes was", result); Assert.DoesNotContain("auto generated", result); @@ -61,7 +61,7 @@ public void Normalize_LegacyFixture_StripsAutoGeneratedFooter() [Fact] public void Normalize_LegacyFixture_StripsRawHtmlWrappers() { - var result = ReleaseNotesNormalizer.Normalize(Constants.GitHubReleaseNotes); + var result = GitHubReleaseNormalizer.Normalize(Constants.GitHubReleaseNotes); Assert.DoesNotContain("", result); @@ -76,7 +76,7 @@ public void Normalize_LegacyShaBullet_StripsCommitId() { const string raw = "* f7f7aff67132dc32c92519a1bc250e1a81606e2b Fixed LF issue in App.xaml"; - var result = ReleaseNotesNormalizer.Normalize(raw); + var result = GitHubReleaseNormalizer.Normalize(raw); Assert.Equal("- Fixed LF issue in App.xaml", result); } @@ -86,7 +86,7 @@ public void Normalize_MarkdownLinkBullet_StripsLinkPrefix() { const string raw = "* [56cc8e7](https://github.com/microsoft/EventLogExpert/commit/56cc8e79f381b38d0ae32467265e3ca77dd16d74) Fixed CI issue"; - var result = ReleaseNotesNormalizer.Normalize(raw); + var result = GitHubReleaseNormalizer.Normalize(raw); Assert.Equal("- Fixed CI issue", result); } @@ -94,9 +94,9 @@ public void Normalize_MarkdownLinkBullet_StripsLinkPrefix() [Fact] public void Normalize_NullOrWhitespace_ReturnsEmpty() { - Assert.Equal(string.Empty, ReleaseNotesNormalizer.Normalize(null)); - Assert.Equal(string.Empty, ReleaseNotesNormalizer.Normalize(string.Empty)); - Assert.Equal(string.Empty, ReleaseNotesNormalizer.Normalize(" \r\n ")); + Assert.Equal(string.Empty, GitHubReleaseNormalizer.Normalize(null)); + Assert.Equal(string.Empty, GitHubReleaseNormalizer.Normalize(string.Empty)); + Assert.Equal(string.Empty, GitHubReleaseNormalizer.Normalize(" \r\n ")); } [Fact] @@ -113,7 +113,7 @@ public void Normalize_RichAuthoredMarkdown_PassesThroughUnchanged() - Fixed crash when opening empty file """; - var result = ReleaseNotesNormalizer.Normalize(raw); + var result = GitHubReleaseNormalizer.Normalize(raw); Assert.Contains("## What's New in v1.2.3", result); Assert.Contains("### Features", result); @@ -124,7 +124,7 @@ public void Normalize_RichAuthoredMarkdown_PassesThroughUnchanged() [Fact] public void Normalize_RichBodyWithBoldTag_PreservesInnerTextOnly() { - var result = ReleaseNotesNormalizer.Normalize("Some important note"); + var result = GitHubReleaseNormalizer.Normalize("Some important note"); Assert.Equal("Some important note", result); } @@ -132,7 +132,7 @@ public void Normalize_RichBodyWithBoldTag_PreservesInnerTextOnly() [Fact] public void Normalize_SummaryWithNonLegacyTags_StripsTagsFromHeading() { - var result = ReleaseNotesNormalizer.Normalize("Click