Skip to content

Commit

Permalink
feat: add dialog to choose reference branch [IDE-601] (#600)
Browse files Browse the repository at this point in the history
* fix: UI thread usage during node searching  /selection

* chore: enable info nodes for OSS and Code always

* fix: save settings when token is received

* feat: add dialog to choose a reference branch

* docs: update CHANGELOG.md

* fix: tests

* chore: add test for branch choosing tree node

* chore: optimize import

* fix: tests

* feat: add test for branch chooser
  • Loading branch information
bastiandoetsch authored Sep 5, 2024
1 parent 337af1a commit 10962dd
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 212 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
- allow annotations during IntelliJ indexing
- add gutter icons for Snyk issues
- add color and highlighting setting for Snyk issues
- add dialog to choose reference branch when delta scanning
- always display info nodes

### Fixes
- add name to code vision provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class SnykApplicationSettingsStateService : PersistentStateComponent<SnykApplica
var cliReleaseChannel = "stable"
var manageBinariesAutomatically: Boolean = true
var fileListenerEnabled: Boolean = true
// TODO migrate to https://plugins.jetbrains.com/docs/intellij/persisting-sensitive-data.html?from=jetbrains.org
var token: String? = null
var customEndpointUrl: String? = null
var organization: String? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import com.intellij.openapi.project.Project
import io.snyk.plugin.events.SnykProductsOrSeverityListener
import io.snyk.plugin.events.SnykResultsFilteringListener
import io.snyk.plugin.events.SnykSettingsListener
import io.snyk.plugin.getContentRootPaths
import io.snyk.plugin.getSnykProjectSettingsService
import io.snyk.plugin.getSnykTaskQueueService
import io.snyk.plugin.getSnykToolWindowPanel
Expand Down Expand Up @@ -104,13 +103,9 @@ class SnykProjectSettingsConfigurable(
val snykProjectSettingsService = getSnykProjectSettingsService(project)
snykProjectSettingsService?.additionalParameters = snykSettingsDialog.getAdditionalParameters()
val fcs = service<FolderConfigSettings>()
project.getContentRootPaths().forEach {
val fc = fcs.getFolderConfig(it.toAbsolutePath().toString())
if (fc != null) {
val newFC = fc.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" "))
fcs.addFolderConfig(newFC)
}
}
fcs.getAllForProject(project)
.map { it.copy(additionalParameters = snykSettingsDialog.getAdditionalParameters().split(" ")) }
.forEach { fcs.addFolderConfig(it) }
}

runBackgroundableTask("processing config changes", project, true) {
Expand Down
83 changes: 83 additions & 0 deletions src/main/kotlin/io/snyk/plugin/ui/BranchChooserComboboxDialog.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package io.snyk.plugin.ui

import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.util.ui.GridBag
import com.intellij.util.ui.JBUI
import org.jetbrains.concurrency.runAsync
import snyk.common.lsp.FolderConfig
import snyk.common.lsp.FolderConfigSettings
import snyk.common.lsp.LanguageServerWrapper
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel


class BranchChooserComboBoxDialog(val project: Project) : DialogWrapper(true) {
var comboBoxes: MutableList<ComboBox<String>> = mutableListOf()

init {
init()
title = "Choose base branch for net-new issue scanning"
}

override fun createCenterPanel(): JComponent {
val folderConfigs = service<FolderConfigSettings>().getAllForProject(project)
folderConfigs.forEach {
val comboBox = ComboBox(it.localBranches.sorted().toTypedArray())
comboBox.selectedItem = it.baseBranch
comboBox.name = it.folderPath
comboBoxes.add(comboBox)
}
val gridBagLayout = GridBagLayout()
val dialogPanel = JPanel(gridBagLayout)
val gridBag = GridBag()
gridBag.defaultFill = GridBagConstraints.HORIZONTAL
gridBag.insets = JBUI.insets(20)
comboBoxes.forEach {
dialogPanel.add(JLabel("Base Branch for ${it.name}: "))
dialogPanel.add(it, gridBag.nextLine())
}
return dialogPanel
}

override fun doOKAction() {
execute()
super.doOKAction()
}

fun execute() {
val folderConfigSettings = service<FolderConfigSettings>()
comboBoxes.forEach {
val folderConfig: FolderConfig? = folderConfigSettings.getFolderConfig(it.name)
if (folderConfig == null) {
SnykBalloonNotificationHelper.showError(
"Unexpectedly cannot retrieve folder config for ${it.name} for base branch updating.",
project
)
return@forEach
}

val baseBranch = it.selectedItem!!.toString() // validation makes sure it is not null and not empty
folderConfigSettings.addFolderConfig(folderConfig.copy(baseBranch = baseBranch))
}
runAsync {
LanguageServerWrapper.getInstance().updateConfiguration()
LanguageServerWrapper.getInstance().sendScanCommand(project)
}
}

override fun doValidate(): ValidationInfo? {
comboBoxes.forEach {
if (it.selectedItem == null || it.selectedItem?.toString()?.isEmpty() == true) {
return ValidationInfo("Please select a base branch for ${it.name}", it)
}
}
return null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.snyk.plugin.ui.toolwindow
import com.intellij.notification.NotificationAction
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.components.Service
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
Expand Down Expand Up @@ -40,6 +41,7 @@ import io.snyk.plugin.pluginSettings
import io.snyk.plugin.refreshAnnotationsForOpenFiles
import io.snyk.plugin.services.SnykApplicationSettingsStateService
import io.snyk.plugin.snykToolWindow
import io.snyk.plugin.ui.BranchChooserComboBoxDialog
import io.snyk.plugin.ui.SnykBalloonNotificationHelper
import io.snyk.plugin.ui.expandTreeNodeRecursively
import io.snyk.plugin.ui.toolwindow.nodes.DescriptionHolderTreeNode
Expand All @@ -52,7 +54,9 @@ import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootTreeNodeBase
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ErrorTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode
import io.snyk.plugin.ui.toolwindow.panels.IssueDescriptionPanel
import io.snyk.plugin.ui.toolwindow.panels.SnykAuthPanel
import io.snyk.plugin.ui.toolwindow.panels.SnykErrorPanel
Expand Down Expand Up @@ -116,7 +120,7 @@ class SnykToolWindowPanel(
* */
private var smartReloadMode = false

var navigateToSourceEnabled = true
var triggerSelectionListeners = true

private val treeNodeStub =
object : RootTreeNodeBase("", project) {
Expand Down Expand Up @@ -328,13 +332,18 @@ class SnykToolWindowPanel(

private fun updateDescriptionPanelBySelectedTreeNode() {
val capturedSmartReloadMode = smartReloadMode
val capturedNavigateToSourceEnabled = navigateToSourceEnabled
val capturedNavigateToSourceEnabled = triggerSelectionListeners

ApplicationManager.getApplication().invokeLater {
descriptionPanel.removeAll()
val selectionPath = vulnerabilitiesTree.selectionPath
if (nonNull(selectionPath)) {
val lastPathComponent = selectionPath!!.lastPathComponent

if (lastPathComponent is ChooseBranchNode && capturedNavigateToSourceEnabled && !capturedSmartReloadMode) {
BranchChooserComboBoxDialog(project).show()
}

if (!capturedSmartReloadMode &&
capturedNavigateToSourceEnabled &&
lastPathComponent is NavigatableToSourceTreeNode
Expand Down Expand Up @@ -883,6 +892,7 @@ class SnykToolWindowPanel(
selectedNodeUserObject: Any?,
) {
val selectedNode = TreeUtil.findNodeWithObject(rootTreeNode, selectedNodeUserObject)
if (selectedNode is InfoTreeNode) return

displayEmptyDescription()
(vulnerabilitiesTree.model as DefaultTreeModel).reload(nodeToReload)
Expand Down Expand Up @@ -910,12 +920,13 @@ class SnykToolWindowPanel(
private fun selectAndDisplayNodeWithIssueDescription(selectCondition: (DefaultMutableTreeNode) -> Boolean) {
val node = TreeUtil.findNode(rootTreeNode) { selectCondition(it) }
if (node != null) {
navigateToSourceEnabled = false
try {
TreeUtil.selectNode(vulnerabilitiesTree, node)
// here TreeSelectionListener is invoked, so no needs for explicit updateDescriptionPanelBySelectedTreeNode()
} finally {
navigateToSourceEnabled = true
invokeLater {
try {
triggerSelectionListeners = false
TreeUtil.selectNode(vulnerabilitiesTree, node)
} finally {
triggerSelectionListeners = true
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.snyk.plugin.ui.toolwindow

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.TextRange
Expand All @@ -19,16 +20,19 @@ import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.CODE_SECURITY_
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NODE_NOT_SUPPORTED_STATE
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.NO_OSS_FILES
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.OSS_ROOT_TEXT
import io.snyk.plugin.ui.toolwindow.SnykToolWindowPanel.Companion.SCANNING_TEXT
import io.snyk.plugin.ui.toolwindow.nodes.leaf.SuggestionTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootContainerIssuesTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootIacIssuesTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootOssTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootQualityIssuesTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.root.RootSecurityIssuesTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.ChooseBranchNode
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.InfoTreeNode
import io.snyk.plugin.ui.toolwindow.nodes.secondlevel.SnykFileTreeNode
import snyk.common.ProductType
import snyk.common.SnykFileIssueComparator
import snyk.common.lsp.FolderConfigSettings
import snyk.common.lsp.ScanIssue
import snyk.common.lsp.SnykScanParams
import javax.swing.JTree
Expand Down Expand Up @@ -70,11 +74,11 @@ class SnykToolWindowSnykScanListenerLS(
ApplicationManager.getApplication().invokeLater {
this.rootSecurityIssuesTreeNode.userObject = "$CODE_SECURITY_ROOT_TEXT (scanning finished)"
this.rootQualityIssuesTreeNode.userObject = "$CODE_QUALITY_ROOT_TEXT (scanning finished)"
this.snykToolWindowPanel.navigateToSourceEnabled = false
this.snykToolWindowPanel.triggerSelectionListeners = false
val snykCachedResults = getSnykCachedResults(project)
displaySnykCodeResults(snykCachedResults?.currentSnykCodeResultsLS ?: emptyMap())
refreshAnnotationsForOpenFiles(project)
this.snykToolWindowPanel.navigateToSourceEnabled = true
this.snykToolWindowPanel.triggerSelectionListeners = true
}
}

Expand All @@ -83,11 +87,11 @@ class SnykToolWindowSnykScanListenerLS(
ApplicationManager.getApplication().invokeLater {
cancelOssIndicator(project)
this.rootOssIssuesTreeNode.userObject = "$OSS_ROOT_TEXT (scanning finished)"
this.snykToolWindowPanel.navigateToSourceEnabled = false
this.snykToolWindowPanel.triggerSelectionListeners = false
val snykCachedResults = getSnykCachedResults(project)
displayOssResults(snykCachedResults?.currentOSSResultsLS ?: emptyMap())
refreshAnnotationsForOpenFiles(project)
this.snykToolWindowPanel.navigateToSourceEnabled = true
this.snykToolWindowPanel.triggerSelectionListeners = true
}
}

Expand Down Expand Up @@ -167,6 +171,7 @@ class SnykToolWindowSnykScanListenerLS(
snykResults = snykResults,
rootNode = this.rootOssIssuesTreeNode,
ossResultsCount = snykResults.values.flatten().distinct().size,
fixableIssuesCount = snykResults.values.flatten().count { it.additionalData.isUpgradable }
)
}

Expand Down Expand Up @@ -204,7 +209,6 @@ class SnykToolWindowSnykScanListenerLS(
addInfoTreeNodes(
rootNode = rootNode,
issues = snykResults.values.flatten().distinct(),
securityIssuesCount = securityIssuesCount,
fixableIssuesCount = fixableIssuesCount,
)

Expand Down Expand Up @@ -259,32 +263,39 @@ class SnykToolWindowSnykScanListenerLS(
fun addInfoTreeNodes(
rootNode: DefaultMutableTreeNode,
issues: List<ScanIssue>,
securityIssuesCount: Int? = null,
fixableIssuesCount: Int? = null,
) {
if (disposed) return

// only add these info tree nodes to Snyk Code security vulnerabilities for now
if (securityIssuesCount == null) {
if (rootNode.userObject == SCANNING_TEXT) {
return
}

val settings = pluginSettings()
// only add these when we enable the consistent ignores flow for now
if (!settings.isGlobalIgnoresFeatureEnabled) {
return
// TODO: check for delta findings
val deltaFindingsEnabled = true
if (deltaFindingsEnabled) {
// we need one choose branch node for each content root. sigh.
service<FolderConfigSettings>().getAllForProject(project).forEach {
val branchChooserTreeNode = ChooseBranchNode(
project = project,
info = "Click to choose base branch for ${it.folderPath} [ current: ${it.baseBranch} ]"
)
rootNode.add(branchChooserTreeNode)
}
}

var text = "✅ Congrats! No vulnerabilities found!"
val issuesCount = issues.size
val ignoredIssuesCount = issues.count { it.isIgnored() }
if (issuesCount != 0) {
if (issuesCount == 1) {
text = "$issuesCount vulnerability found by Snyk"
text = if (issuesCount == 1) {
"$issuesCount vulnerability found by Snyk"
} else {
text = "$issuesCount vulnerabilities found by Snyk"
"$issuesCount vulnerabilities found by Snyk"
}
if (pluginSettings().isGlobalIgnoresFeatureEnabled) {
text += ", $ignoredIssuesCount ignored"
}
text += ", $ignoredIssuesCount ignored"
}
rootNode.add(
InfoTreeNode(
Expand All @@ -297,31 +308,32 @@ class SnykToolWindowSnykScanListenerLS(
if (fixableIssuesCount > 0) {
rootNode.add(
InfoTreeNode(
"$fixableIssuesCount vulnerabilities can be fixed by Snyk DeepCode AI",
"$fixableIssuesCount vulnerabilities can be fixed automatically",
project,
),
)
} else {
rootNode.add(
InfoTreeNode("There are no vulnerabilities fixable by Snyk DeepCode AI", project),
InfoTreeNode("There are no vulnerabilities automatically fixable", project),
)
}
}

if (ignoredIssuesCount == issuesCount && !settings.ignoredIssuesEnabled) {
rootNode.add(
InfoTreeNode(
"Adjust your Issue View Options to see ignored issues.",
project,
),
)
} else if (ignoredIssuesCount == 0 && !settings.openIssuesEnabled) {
rootNode.add(
InfoTreeNode(
"Adjust your Issue View Options to open issues.",
project,
),
)
if (pluginSettings().isGlobalIgnoresFeatureEnabled) {
if (ignoredIssuesCount == issuesCount && !settings.ignoredIssuesEnabled) {
rootNode.add(
InfoTreeNode(
"Adjust your Issue View Options to see ignored issues.",
project,
),
)
} else if (ignoredIssuesCount == 0 && !settings.openIssuesEnabled) {
rootNode.add(
InfoTreeNode(
"Adjust your Issue View Options to open issues.",
project,
),
)
}
}
}

Expand Down
Loading

0 comments on commit 10962dd

Please sign in to comment.