From 9053114c7e9de73be777a5413b8cd23b80a8484c Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Wed, 19 Feb 2025 17:14:40 +0100 Subject: [PATCH 1/2] CM-44176 - Add tree view filtering by severity --- CHANGELOG.md | 1 + .../TreeViewFilterBySeverityCommands.cs | 81 +++++++++++++++++++ .../TreeView/CycodeTreeViewControl.xaml.cs | 31 ++++++- ...de.VisualStudio.Extension.Shared.projitems | 1 + .../CycodePackage.cs | 20 ++++- .../Services/TemporaryStateService.cs | 12 +++ .../VSCommandTable.cs | 6 ++ .../VSCommandTable.vsct | 61 ++++++++++++++ 8 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2f1cd..1d00ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ## [Unreleased] +- Add tree view filtering by severity - Add proper support for disabled modules - Fix scrolling issue in violation panels diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs new file mode 100644 index 0000000..65518ed --- /dev/null +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs @@ -0,0 +1,81 @@ +using Cycode.VisualStudio.Extension.Shared.DTO; +using Cycode.VisualStudio.Extension.Shared.Services; + +namespace Cycode.VisualStudio.Extension.Shared.Commands; + +internal static class TreeViewFilterBySeverityCommand { + public static void Execute(OleMenuCommand command, string severity) { + ITemporaryStateService tempState = + ServiceLocator.GetService(); + IToolWindowMessengerService toolWindowMessengerService = + ServiceLocator.GetService(); + + switch (severity.ToLower()) { + case "critical": + tempState.IsTreeViewFilterByCriticalSeverityEnabled = !command.Checked; + break; + case "high": + tempState.IsTreeViewFilterByHighSeverityEnabled = !command.Checked; + break; + case "medium": + tempState.IsTreeViewFilterByMediumSeverityEnabled = !command.Checked; + break; + case "low": + tempState.IsTreeViewFilterByLowSeverityEnabled = !command.Checked; + break; + case "info": + tempState.IsTreeViewFilterByInfoSeverityEnabled = !command.Checked; + break; + } + + command.Checked = !command.Checked; // toggle checked state + + // refresh tree view to apply filter + toolWindowMessengerService.Send(new MessageEventArgs(MessengerCommand.TreeViewRefresh)); + } +} + +[Command(PackageIds.TreeViewFilterByCriticalSeverityCommand)] +internal sealed class TreeViewFilterByCriticalSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "critical"); + } + } +} + +[Command(PackageIds.TreeViewFilterByHighSeverityCommand)] +internal sealed class TreeViewFilterByHighSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "high"); + } + } +} + +[Command(PackageIds.TreeViewFilterByMediumSeverityCommand)] +internal sealed class TreeViewFilterByMediumSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "medium"); + } + } +} + +[Command(PackageIds.TreeViewFilterByLowSeverityCommand)] +internal sealed class TreeViewFilterByLowSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "low"); + } + } +} + +[Command(PackageIds.TreeViewFilterByInfoSeverityCommand)] +internal sealed class TreeViewFilterByInfoSeverityCommand : BaseCommand { + protected override void Execute(object sender, EventArgs e) { + if (sender is OleMenuCommand command) { + TreeViewFilterBySeverityCommand.Execute(command, "info"); + } + } +} diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs index 6fcefe2..0c16cda 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs @@ -22,6 +22,9 @@ public partial class CycodeTreeViewControl { private static readonly IToolWindowMessengerService _toolWindowMessengerService = ServiceLocator.GetService(); + + private static readonly ITemporaryStateService _tempState = + ServiceLocator.GetService(); private static readonly ILoggerService _logger = ServiceLocator.GetService(); @@ -90,6 +93,28 @@ private static int GetSeverityWeight(string severity) { _ => 0 }; } + + private HashSet GetEnabledSeverityFilters() { + HashSet enabledSeverityFilters = []; + + if (_tempState.IsTreeViewFilterByCriticalSeverityEnabled) { + enabledSeverityFilters.Add("critical"); + } + if (_tempState.IsTreeViewFilterByHighSeverityEnabled) { + enabledSeverityFilters.Add("high"); + } + if (_tempState.IsTreeViewFilterByMediumSeverityEnabled) { + enabledSeverityFilters.Add("medium"); + } + if (_tempState.IsTreeViewFilterByLowSeverityEnabled) { + enabledSeverityFilters.Add("low"); + } + if (_tempState.IsTreeViewFilterByInfoSeverityEnabled) { + enabledSeverityFilters.Add("info"); + } + + return enabledSeverityFilters; + } private static string GetRootNodeSummary(IEnumerable sortedDetections) { // detections must be sorted by severity already @@ -115,7 +140,11 @@ private void CreateDetectionNodes( IEnumerable detections, Func createNodeCallback ) { - List sortedDetections = detections + HashSet enabledSeverityFilters = GetEnabledSeverityFilters(); + List severityFilteredDetections = detections + .Where(detection => !enabledSeverityFilters.Contains(detection.Severity.ToLower())) + .ToList(); + List sortedDetections = severityFilteredDetections .OrderByDescending(detection => GetSeverityWeight(detection.Severity)) .ToList(); IEnumerable> detectionsByFile = diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems b/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems index 1438902..d0bd3f9 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Cycode.VisualStudio.Extension.Shared.projitems @@ -49,6 +49,7 @@ + diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs index e0b5329..129ae41 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/CycodePackage.cs @@ -2,9 +2,9 @@ global using Community.VisualStudio.Toolkit; global using Microsoft.VisualStudio.Shell; global using Task = System.Threading.Tasks.Task; +using System.ComponentModel.Design; using System.Runtime.InteropServices; using System.Threading; -using Community.VisualStudio.Toolkit; using Cycode.VisualStudio.Extension.Shared.Components.ToolWindows; using Cycode.VisualStudio.Extension.Shared.Options; using Cycode.VisualStudio.Extension.Shared.Sentry; @@ -12,7 +12,6 @@ using Cycode.VisualStudio.Extension.Shared.Services.ErrorList; using Cycode.VisualStudio.Extension.Shared.Services.ErrorTagger; using Microsoft.Extensions.DependencyInjection; -using Microsoft.VisualStudio.Shell; namespace Cycode.VisualStudio.Extension.Shared; @@ -64,6 +63,23 @@ protected override async Task InitializeAsync( cycodeService.InstallCliIfNeededAndCheckAuthenticationAsync().FireAndForget(); logger.Info("CycodePackage.InitializeAsync completed."); + + // set the filter commands to checked because they are enabled by default (no actual filtering) + if (await GetServiceAsync(typeof(IMenuCommandService)) is OleMenuCommandService commandService) { + int[] filterCommands = [ + PackageIds.TreeViewFilterByCriticalSeverityCommand, + PackageIds.TreeViewFilterByHighSeverityCommand, + PackageIds.TreeViewFilterByMediumSeverityCommand, + PackageIds.TreeViewFilterByLowSeverityCommand, + PackageIds.TreeViewFilterByInfoSeverityCommand + ]; + foreach (int commandId in filterCommands) { + MenuCommand cmd = commandService.FindCommand(new CommandID(PackageGuids.Cycode, commandId)); + if (cmd != null) { + cmd.Checked = true; + } + } + } } private static void OnSettingsSaved(General obj) { diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs index ef75c9d..5f6ce70 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Services/TemporaryStateService.cs @@ -13,6 +13,12 @@ public interface ITemporaryStateService { bool IsIacScanningEnabled { get; } bool IsSastScanningEnabled { get; } bool IsAiLargeLanguageModelEnabled { get; } + + bool IsTreeViewFilterByCriticalSeverityEnabled { get; set; } + bool IsTreeViewFilterByHighSeverityEnabled { get; set; } + bool IsTreeViewFilterByMediumSeverityEnabled { get; set; } + bool IsTreeViewFilterByLowSeverityEnabled { get; set; } + bool IsTreeViewFilterByInfoSeverityEnabled { get; set; } } public class TemporaryStateService : ITemporaryStateService { @@ -37,6 +43,12 @@ public StatusResult CliStatus { public bool IsSastScanningEnabled => _cliStatus?.SupportedModules?.SastScanning == true; public bool IsAiLargeLanguageModelEnabled => _cliStatus?.SupportedModules?.AiLargeLanguageModel == true; + public bool IsTreeViewFilterByCriticalSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByHighSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByMediumSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByLowSeverityEnabled { get; set; } = false; + public bool IsTreeViewFilterByInfoSeverityEnabled { get; set; } = false; + public TemporaryStateService(ILoggerService loggerService) { _loggerService = loggerService; _loggerService.Info("CycodeTemporaryStateService init"); diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs index dd1c8ea..59b4e7b 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs @@ -23,6 +23,12 @@ internal sealed class PackageIds { public const int TreeViewExpandAllCommand = 0x1057; public const int TreeViewCollapseAllCommand = 0x1058; + public const int TreeViewFilterByCriticalSeverityCommand = 0x1059; + public const int TreeViewFilterByHighSeverityCommand = 0x1060; + public const int TreeViewFilterByMediumSeverityCommand = 0x1061; + public const int TreeViewFilterByLowSeverityCommand = 0x1062; + public const int TreeViewFilterByInfoSeverityCommand = 0x1063; + public const int TopMenuCycodeCommand = 0x1103; public const int TopMenuOpenSettingsCommand = 0x1104; } \ No newline at end of file diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct index 86d59bd..2407ed6 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct @@ -83,6 +83,41 @@ Open documentation + + + + + @@ -117,6 +152,11 @@ + + + + + @@ -134,6 +174,12 @@ + + + + + + @@ -158,5 +204,20 @@ + + + + + + + + + + + + + + + From 0518d84cd631decfb37c7978add6f2763b63232b Mon Sep 17 00:00:00 2001 From: Ilya Siamionau Date: Thu, 20 Feb 2025 21:12:45 +0100 Subject: [PATCH 2/2] divide buttons into groups; fix bool logic around checked state --- .../TreeViewFilterBySeverityCommands.cs | 21 +++-- .../TreeView/CycodeTreeViewControl.xaml.cs | 2 +- .../VSCommandTable.cs | 3 +- .../VSCommandTable.vsct | 91 ++++++++++++------- 4 files changed, 73 insertions(+), 44 deletions(-) diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs index 65518ed..33f01ee 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Commands/TreeViewFilterBySeverityCommands.cs @@ -10,26 +10,33 @@ public static void Execute(OleMenuCommand command, string severity) { IToolWindowMessengerService toolWindowMessengerService = ServiceLocator.GetService(); + /* + * We must flip the bool first to reflect the new state. + * + * Unchecked means that we do not want to see that severity in the tree view. + * In other words, if the button highlighted, it means that we WANT to see that severity in the tree view. + */ + command.Checked = !command.Checked; + bool isFilterEnabled = !command.Checked; + switch (severity.ToLower()) { case "critical": - tempState.IsTreeViewFilterByCriticalSeverityEnabled = !command.Checked; + tempState.IsTreeViewFilterByCriticalSeverityEnabled = isFilterEnabled; break; case "high": - tempState.IsTreeViewFilterByHighSeverityEnabled = !command.Checked; + tempState.IsTreeViewFilterByHighSeverityEnabled = isFilterEnabled; break; case "medium": - tempState.IsTreeViewFilterByMediumSeverityEnabled = !command.Checked; + tempState.IsTreeViewFilterByMediumSeverityEnabled = isFilterEnabled; break; case "low": - tempState.IsTreeViewFilterByLowSeverityEnabled = !command.Checked; + tempState.IsTreeViewFilterByLowSeverityEnabled = isFilterEnabled; break; case "info": - tempState.IsTreeViewFilterByInfoSeverityEnabled = !command.Checked; + tempState.IsTreeViewFilterByInfoSeverityEnabled = isFilterEnabled; break; } - command.Checked = !command.Checked; // toggle checked state - // refresh tree view to apply filter toolWindowMessengerService.Send(new MessageEventArgs(MessengerCommand.TreeViewRefresh)); } diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs index 0c16cda..5304b0f 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/Components/TreeView/CycodeTreeViewControl.xaml.cs @@ -94,7 +94,7 @@ private static int GetSeverityWeight(string severity) { }; } - private HashSet GetEnabledSeverityFilters() { + private static HashSet GetEnabledSeverityFilters() { HashSet enabledSeverityFilters = []; if (_tempState.IsTreeViewFilterByCriticalSeverityEnabled) { diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs index 59b4e7b..226ba79 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.cs @@ -14,7 +14,8 @@ internal sealed class PackageGuids { internal sealed class PackageIds { public const int ViewOpenToolWindowCommand = 0x1001; - public const int TWindowToolbar = 0x1050; + public const int TWindowToolbar = 0x1030; + public const int ToolbarOpenSettingsCommand = 0x1052; public const int ToolbarRunAllScansCommand = 0x1053; public const int ToolbarOpenWebDocsCommand = 0x1054; diff --git a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct index 2407ed6..2fb7c0a 100644 --- a/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct +++ b/src/extension/Cycode.VisualStudio.Extension.Shared/VSCommandTable.vsct @@ -33,95 +33,110 @@ - - - + + + - + + + + + + + + + + + + + + + + @@ -164,8 +179,14 @@ - - + + + + + + + +