diff --git a/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs b/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs new file mode 100644 index 00000000..ff058a3e --- /dev/null +++ b/src/EventLogExpert.Eventing.Tests/Helpers/LogNamesTests.cs @@ -0,0 +1,36 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +using EventLogExpert.Eventing.Helpers; + +namespace EventLogExpert.Eventing.Tests.Helpers; + +public sealed class LogNamesTests +{ + [Fact] + public void AdminOnlyLiveLogNames_ContainsExpectedNames() + { + Assert.Contains(LogNames.SecurityLog, LogNames.AdminOnlyLiveLogNames); + Assert.Contains(LogNames.StateLog, LogNames.AdminOnlyLiveLogNames); + Assert.Equal(2, LogNames.AdminOnlyLiveLogNames.Count); + } + + [Theory] + [InlineData("security")] + [InlineData("SECURITY")] + [InlineData("state")] + [InlineData("STATE")] + public void AdminOnlyLiveLogNames_ShouldMatchCaseInsensitively(string input) + { + Assert.Contains(input, LogNames.AdminOnlyLiveLogNames); + } + + [Fact] + public void Constants_HaveExpectedValues() + { + Assert.Equal("Application", LogNames.ApplicationLog); + Assert.Equal("Security", LogNames.SecurityLog); + Assert.Equal("State", LogNames.StateLog); + Assert.Equal("System", LogNames.SystemLog); + } +} diff --git a/src/EventLogExpert.Eventing/Helpers/LogNames.cs b/src/EventLogExpert.Eventing/Helpers/LogNames.cs new file mode 100644 index 00000000..74996b29 --- /dev/null +++ b/src/EventLogExpert.Eventing/Helpers/LogNames.cs @@ -0,0 +1,19 @@ +// // Copyright (c) Microsoft Corporation. +// // Licensed under the MIT License. + +namespace EventLogExpert.Eventing.Helpers; + +public static class LogNames +{ + public const string ApplicationLog = "Application"; + public const string SecurityLog = "Security"; + public const string StateLog = "State"; + public const string SystemLog = "System"; + + /// Live event log names that require process elevation to read. + public static IReadOnlySet AdminOnlyLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + SecurityLog, + StateLog, + }; +} diff --git a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs index 21cc0d6d..bebc81e4 100644 --- a/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs +++ b/src/EventLogExpert.Eventing/Providers/RegistryProvider.cs @@ -34,7 +34,7 @@ public IEnumerable GetMessageFilesForLegacyProvider(string providerName) foreach (var logSubKeyName in eventLogKey.GetSubKeyNames()) { // Skip Security and State since it requires elevation - if (logSubKeyName is "Security" or "State") + if (LogNames.AdminOnlyLiveLogNames.Contains(logSubKeyName)) { continue; } diff --git a/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs b/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs index d480e087..b46eaf0f 100644 --- a/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs +++ b/src/EventLogExpert.UI.Tests/LogNameMethodsTests.cs @@ -119,4 +119,22 @@ public void GetMenuPath_WhenSimpleRootLog_ShouldReturnSingleSegment() Assert.Equal(["Application"], path); } + + [Fact] + public void HardCodedLiveLogNames_ContainsExpectedNames() + { + Assert.Contains("Application", LogNameMethods.HardCodedLiveLogNames); + Assert.Contains("System", LogNameMethods.HardCodedLiveLogNames); + Assert.Contains("Security", LogNameMethods.HardCodedLiveLogNames); + Assert.Equal(3, LogNameMethods.HardCodedLiveLogNames.Count); + } + + [Theory] + [InlineData("application")] + [InlineData("APPLICATION")] + [InlineData("Application")] + public void HardCodedLiveLogNames_ShouldMatchCaseInsensitively(string input) + { + Assert.Contains(input, LogNameMethods.HardCodedLiveLogNames); + } } diff --git a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs index ee1ad2ce..9c35162a 100644 --- a/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs +++ b/src/EventLogExpert.UI/Interfaces/IMenuActionService.cs @@ -30,13 +30,13 @@ public interface IMenuActionService Task OpenDocsAsync(); - Task OpenFileAsync(bool addLog); + Task OpenFileAsync(bool combineLog); - Task OpenFolderAsync(bool addLog); + Task OpenFolderAsync(bool combineLog); Task OpenIssueAsync(); - Task OpenLiveLogAsync(string logName, bool addLog); + Task OpenLiveLogAsync(string logName, bool combineLog); Task OpenSettingsAsync(); diff --git a/src/EventLogExpert.UI/LogNameMethods.cs b/src/EventLogExpert.UI/LogNameMethods.cs index 1c57ef93..02e6524c 100644 --- a/src/EventLogExpert.UI/LogNameMethods.cs +++ b/src/EventLogExpert.UI/LogNameMethods.cs @@ -1,12 +1,22 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Helpers; + namespace EventLogExpert.UI; public static class LogNameMethods { private const string MicrosoftWindowsPrefix = "Microsoft-Windows-"; + /// Live event log names hard-coded in MenuBar's File menu — filter these from dynamic log enumeration to avoid duplicates. + public static IReadOnlySet HardCodedLiveLogNames { get; } = new HashSet(StringComparer.OrdinalIgnoreCase) + { + LogNames.ApplicationLog, + LogNames.SystemLog, + LogNames.SecurityLog, + }; + public static IReadOnlyList GetMenuPath(string logName) { if (string.IsNullOrWhiteSpace(logName)) diff --git a/src/EventLogExpert/Services/MauiMenuActionService.cs b/src/EventLogExpert/Services/MauiMenuActionService.cs index 18162b7c..e000e329 100644 --- a/src/EventLogExpert/Services/MauiMenuActionService.cs +++ b/src/EventLogExpert/Services/MauiMenuActionService.cs @@ -114,6 +114,7 @@ public async Task> GetOtherLogNamesAsync() _cachedLogNames = await Task.Run>(() => EventLogSession.GlobalSession.GetLogNames() + .Where(name => !LogNameMethods.HardCodedLiveLogNames.Contains(name)) .OrderBy(name => name, StringComparer.OrdinalIgnoreCase) .ToList()); @@ -130,7 +131,7 @@ public async Task> GetOtherLogNamesAsync() public Task OpenDocsAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/blob/main/docs/Home.md"); - public async Task OpenFileAsync(bool addLog) + public async Task OpenFileAsync(bool combineLog) { var options = new PickOptions { @@ -142,7 +143,7 @@ public async Task OpenFileAsync(bool addLog) if (!files.Any()) { return; } - if (!addLog) + if (!combineLog) { await CloseAllLogsAsync(); } @@ -155,7 +156,7 @@ public async Task OpenFileAsync(bool addLog) } } - public async Task OpenFolderAsync(bool addLog) + public async Task OpenFolderAsync(bool combineLog) { string? folderPath = await FolderPickerHelper.PickFolderAsync(); @@ -165,7 +166,7 @@ public async Task OpenFolderAsync(bool addLog) if (files.Count == 0) { return; } - if (!addLog) + if (!combineLog) { await CloseAllLogsAsync(); } @@ -178,13 +179,13 @@ public async Task OpenFolderAsync(bool addLog) public Task OpenIssueAsync() => OpenBrowserAsync("https://github.com/microsoft/EventLogExpert/issues/new"); - public Task OpenLiveLogAsync(string logName, bool addLog) => OpenLogAsync(logName, PathType.LogName, addLog); + public Task OpenLiveLogAsync(string logName, bool combineLog) => OpenLogAsync(logName, PathType.LogName, combineLog); - public async Task OpenLogAsync(string logPath, PathType pathType, bool shouldAddLog = false) + public async Task OpenLogAsync(string logPath, PathType pathType, bool combineLog = false) { if (string.IsNullOrWhiteSpace(logPath)) { return; } - if (shouldAddLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath)) { return; } + if (combineLog && _eventLogState.Value.ActiveLogs.ContainsKey(logPath)) { return; } EventLogInformation? eventLogInformation; @@ -214,7 +215,7 @@ await _dialogService.ShowAlert( return; } - if (!shouldAddLog) + if (!combineLog) { await _cancellationTokenSource.CancelAsync(); _dispatcher.Dispatch(new EventLogAction.CloseAll()); diff --git a/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs b/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs index e5eeb1ca..fffb5835 100644 --- a/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs +++ b/src/EventLogExpert/Shared/Components/Menu/MenuBar.razor.cs @@ -1,8 +1,10 @@ // // Copyright (c) Microsoft Corporation. // // Licensed under the MIT License. +using EventLogExpert.Eventing.Helpers; using EventLogExpert.UI; using EventLogExpert.UI.Interfaces; +using EventLogExpert.UI.Services; using EventLogExpert.UI.Store.EventLog; using EventLogExpert.UI.Store.FilterPane; using Fluxor; @@ -16,6 +18,7 @@ namespace EventLogExpert.Shared.Components.Menu; public sealed partial class MenuBar : IDisposable { private readonly List _bars = []; + private ElementReference[] _barElements = []; private int _focusedBarIndex; private long _openRequestId; @@ -27,9 +30,14 @@ public sealed partial class MenuBar : IDisposable [Inject] private IStateSelection ContinuouslyUpdate { get; init; } = null!; + [Inject] private ICurrentVersionProvider CurrentVersionProvider { get; init; } = null!; + [Inject] private IStateSelection FilterPaneIsEnabled { get; init; } = null!; + [Inject] + private IStateSelection HasActiveLogs { get; init; } = null!; + [Inject] private IJSRuntime JSRuntime { get; init; } = null!; [Inject] private IMenuService MenuService { get; init; } = null!; @@ -49,6 +57,7 @@ protected override void OnInitialized() { ContinuouslyUpdate.Select(state => state.ContinuouslyUpdate); FilterPaneIsEnabled.Select(state => state.IsEnabled); + HasActiveLogs.Select(state => !state.ActiveLogs.IsEmpty); Settings.CopyTypeChanged += OnSettingsChanged; MenuService.StateChanged += OnMenuServiceStateChanged; @@ -86,14 +95,19 @@ private IReadOnlyList BuildEdit() ]; } - private IReadOnlyList BuildFile() => - [ - MenuItem.SubMenu("Open", BuildOpenSubMenu(false)), - MenuItem.SubMenu("Add Another Log To This View", BuildOpenSubMenu(true)), - MenuItem.Separator(), - MenuItem.Item("Close All Open Logs", () => Actions.CloseAllLogsAsync()), - MenuItem.Item("Exit", Actions.Exit), - ]; + private IReadOnlyList BuildFile() + { + bool hasActiveLogs = HasActiveLogs.Value; + + return + [ + MenuItem.SubMenu("Open", BuildOpenSubMenu(false)), + MenuItem.SubMenu("Combine", BuildOpenSubMenu(true), isEnabled: hasActiveLogs), + MenuItem.Separator(), + MenuItem.Item("Close All", () => Actions.CloseAllLogsAsync(), isEnabled: hasActiveLogs), + MenuItem.Item("Exit", Actions.Exit), + ]; + } private IReadOnlyList BuildHelp() => [ @@ -104,22 +118,27 @@ private IReadOnlyList BuildHelp() => MenuItem.Item("View Logs", () => Actions.ShowDebugLogsAsync()), ]; - private IReadOnlyList BuildOpenSubMenu(bool addLog) => - [ - MenuItem.Item("File", () => Actions.OpenFileAsync(addLog), addLog ? null : "Ctrl+O"), - MenuItem.Item("Folder", () => Actions.OpenFolderAsync(addLog)), - MenuItem.SubMenu("Live Event Log", + private IReadOnlyList BuildOpenSubMenu(bool combineLog) + { + bool isAdmin = CurrentVersionProvider.IsAdmin; + + return [ - MenuItem.Item("Application", () => Actions.OpenLiveLogAsync("Application", addLog)), - MenuItem.Item("System", () => Actions.OpenLiveLogAsync("System", addLog)), - MenuItem.Item("Security", () => Actions.OpenLiveLogAsync("Security", addLog)), - MenuItem.AsyncSubMenu( - "Other Logs", - async () => BuildOtherLogsTree(await Actions.GetOtherLogNamesAsync(), addLog)), - ]), - ]; + MenuItem.Item("File", () => Actions.OpenFileAsync(combineLog), combineLog ? null : "Ctrl+O"), + MenuItem.Item("Folder", () => Actions.OpenFolderAsync(combineLog)), + MenuItem.SubMenu("Live", + [ + MenuItem.Item(LogNames.ApplicationLog, () => Actions.OpenLiveLogAsync(LogNames.ApplicationLog, combineLog)), + MenuItem.Item(LogNames.SystemLog, () => Actions.OpenLiveLogAsync(LogNames.SystemLog, combineLog)), + MenuItem.Item(LogNames.SecurityLog, () => Actions.OpenLiveLogAsync(LogNames.SecurityLog, combineLog), isEnabled: isAdmin), + MenuItem.AsyncSubMenu( + "Other Logs", + async () => BuildOtherLogsTree(await Actions.GetOtherLogNamesAsync(), combineLog, isAdmin)), + ]), + ]; + } - private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logNames, bool addLog) + private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logNames, bool combineLog, bool isAdmin) { var rootChildren = new List(); var folderMap = new Dictionary>(StringComparer.OrdinalIgnoreCase); @@ -130,12 +149,13 @@ private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logName if (path.Count == 0) { continue; } - var leafLabel = path[^1]; - var leaf = MenuItem.Item(leafLabel, () => Actions.OpenLiveLogAsync(logName, addLog)); + var log = path[^1]; + var logIsEnabled = isAdmin || !LogNames.AdminOnlyLiveLogNames.Contains(logName); + var logMenuItem = MenuItem.Item(log, () => Actions.OpenLiveLogAsync(logName, combineLog), isEnabled: logIsEnabled); if (path.Count == 1) { - rootChildren.Add(leaf); + rootChildren.Add(logMenuItem); continue; } @@ -161,7 +181,7 @@ private IReadOnlyList BuildOtherLogsTree(IReadOnlyList logName children = newChildren; } - children.Add(leaf); + children.Add(logMenuItem); } return rootChildren;