Skip to content

Commit 0ccff9b

Browse files
authored
CM-40766 - Add missing toolbar actions: run all, expand all, collapse all (#107)
1 parent bfe9e5b commit 0ccff9b

13 files changed

+174
-55
lines changed

package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@
3636
}
3737
],
3838
"view/title": [
39+
{
40+
"command": "cycode.runAllScans",
41+
"when": "view =~ /^cycode./",
42+
"group": "navigation@0"
43+
},
44+
{
45+
"command": "cycode.treeViewCollapseAllCommand",
46+
"when": "view == cycode.view.tree",
47+
"group": "navigation@1"
48+
},
49+
{
50+
"command": "cycode.treeViewExpandAllCommand",
51+
"when": "view == cycode.view.tree",
52+
"group": "navigation@1"
53+
},
3954
{
4055
"command": "cycode.openMainMenu",
4156
"when": "view == cycode.view.tree",
@@ -162,6 +177,24 @@
162177
"title": "Return to Home Screen",
163178
"icon": "$(arrow-left)"
164179
},
180+
{
181+
"command": "cycode.runAllScans",
182+
"category": "Cycode",
183+
"title": "Run all scans types for the entire project",
184+
"icon": "$(testing-run-icon)"
185+
},
186+
{
187+
"command": "cycode.treeViewExpandAllCommand",
188+
"category": "Cycode",
189+
"title": "Expand All Nodes in Tree View",
190+
"icon": "$(expand-all)"
191+
},
192+
{
193+
"command": "cycode.treeViewCollapseAllCommand",
194+
"category": "Cycode",
195+
"title": "Collapse All Nodes in Tree View",
196+
"icon": "$(collapse-all)"
197+
},
165198
{
166199
"command": "cycode.auth",
167200
"title": "Authenticate with service"

src/commands/index.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ import iacScanCommand from './iac-scan-command';
88
import iacScanForCurrentProjectCommand from './iac-scan-for-current-project-command';
99
import sastScanCommand from './sast-scan-command';
1010
import sastScanForCurrentProjectCommand from './sast-scan-for-current-project-command';
11-
import onTreeItemClickCommand from './on-tree-item-click-command';
1211
import openViolationInFileCommand from './open-violation-in-file-command';
1312
import openViolationPanelCommand from './open-violation-panel-command';
1413
import openSettingsCommand from './open-settings-command';
1514
import openMainMenuCommand from './open-main-menu-command';
15+
import onTreeViewDetectionNodeClickCommand from './on-tree-view-detection-node-click-command';
16+
import treeViewExpandAllCommand from './tree-view-expand-all-command';
17+
import treeViewCollapseAllCommand from './tree-view-collapse-all-command';
18+
import runAllScansCommand from './run-all-scans-command';
1619

1720
export enum VscodeCommands {
1821
SecretScanCommandId = 'cycode.secretScan',
@@ -22,19 +25,25 @@ export enum VscodeCommands {
2225
IacScanForProjectCommandId = 'cycode.iacScanForProject',
2326
SastScanCommandId = 'cycode.sastScan',
2427
SastScanForProjectCommandId = 'cycode.sastScanForProject',
28+
RunAllScansCommandId = 'cycode.runAllScans',
2529

2630
AuthCommandId = 'cycode.auth',
2731
IgnoreCommandId = 'cycode.ignore',
2832

2933
OpenSettingsCommandId = 'cycode.openSettings',
3034
OpenMainMenuCommandId = 'cycode.openMainMenu',
3135

32-
ShowProblemsTab = 'workbench.action.problems.focus',
33-
ShowCycodeView = 'workbench.view.extension.cycode',
34-
3536
OpenViolationInFile = 'cycode.openViolationInFile',
3637
OpenViolationPanel = 'cycode.openViolationPanel',
37-
OnTreeItemClick = 'cycode.onTreeItemClick',
38+
39+
OnTreeViewDetectionNodeClickCommand = 'cycode.onTreeViewDetectionNodeClickCommand',
40+
TreeViewExpandAllCommand = 'cycode.treeViewExpandAllCommand',
41+
TreeViewCollapseAllCommand = 'cycode.treeViewCollapseAllCommand',
42+
43+
// built-in or created automatically by vscode:
44+
WorkbenchShowProblemsTab = 'workbench.action.problems.focus',
45+
WorkbenchShowCycodeView = 'workbench.view.extension.cycode',
46+
WorkbenchTreeViewCollapseAll = 'workbench.actions.treeView.cycode.view.tree.collapseAll',
3847
}
3948

4049
const _VS_CODE_COMMANDS_ID_TO_CALLBACK: Record<string, (...args: never[]) => unknown> = {
@@ -47,11 +56,14 @@ const _VS_CODE_COMMANDS_ID_TO_CALLBACK: Record<string, (...args: never[]) => unk
4756
[VscodeCommands.IacScanForProjectCommandId]: iacScanForCurrentProjectCommand,
4857
[VscodeCommands.SastScanCommandId]: sastScanCommand,
4958
[VscodeCommands.SastScanForProjectCommandId]: sastScanForCurrentProjectCommand,
50-
[VscodeCommands.OnTreeItemClick]: onTreeItemClickCommand,
59+
[VscodeCommands.RunAllScansCommandId]: runAllScansCommand,
5160
[VscodeCommands.OpenViolationInFile]: openViolationInFileCommand,
5261
[VscodeCommands.OpenViolationPanel]: openViolationPanelCommand,
5362
[VscodeCommands.OpenSettingsCommandId]: openSettingsCommand,
5463
[VscodeCommands.OpenMainMenuCommandId]: openMainMenuCommand,
64+
[VscodeCommands.OnTreeViewDetectionNodeClickCommand]: onTreeViewDetectionNodeClickCommand,
65+
[VscodeCommands.TreeViewExpandAllCommand]: treeViewExpandAllCommand,
66+
[VscodeCommands.TreeViewCollapseAllCommand]: treeViewCollapseAllCommand,
5567
};
5668

5769
export const registerCommands = (context: vscode.ExtensionContext): void => {

src/commands/run-all-scans-command.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as vscode from 'vscode';
2+
import { container } from 'tsyringe';
3+
import { validateConfig } from '../utils/config';
4+
import { CycodeService, ICycodeService } from '../services/cycode-service';
5+
import { CliScanType } from '../cli/models/cli-scan-type';
6+
import { IStateService } from '../services/state-service';
7+
import { StateServiceSymbol } from '../symbols';
8+
9+
export default async () => {
10+
if (validateConfig()) {
11+
return;
12+
}
13+
14+
const stateService = container.resolve<IStateService>(StateServiceSymbol);
15+
if (!stateService.globalState.CliAuthed) {
16+
vscode.window.showErrorMessage('Please authenticate with Cycode first');
17+
return;
18+
}
19+
20+
const cycodeService = container.resolve<ICycodeService>(CycodeService);
21+
22+
const scanPromises = [];
23+
scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Secret));
24+
scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Sca));
25+
scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Iac));
26+
scanPromises.push(cycodeService.startScanForCurrentProject(CliScanType.Sast));
27+
await Promise.all(scanPromises);
28+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { container } from 'tsyringe';
2+
import { IExtensionService } from '../services/extension-service';
3+
import { ExtensionServiceSymbol } from '../symbols';
4+
5+
export default async () => {
6+
const extension = container.resolve<IExtensionService>(ExtensionServiceSymbol);
7+
await extension.treeDataProvider.collapseAll();
8+
};
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { container } from 'tsyringe';
2+
import { IExtensionService } from '../services/extension-service';
3+
import { ExtensionServiceSymbol } from '../symbols';
4+
5+
export default async () => {
6+
const extension = container.resolve<IExtensionService>(ExtensionServiceSymbol);
7+
await extension.treeDataProvider.expandAll();
8+
};

src/extension.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { ILoggerService } from './services/logger-service';
2222
import { IScanResultsService } from './services/scan-results-service';
2323
import { IExtensionService } from './services/extension-service';
2424
import { onProjectOpen } from './listeners/on-project-open';
25-
import { createTreeView } from './providers/tree-data';
25+
import { createTreeDataProvider } from './providers/tree-data';
2626
import { registerCommands } from './commands';
2727
import { registerOnDidSaveTextDocument } from './listeners/on-did-save-text-document';
2828
import { registerActivityBar } from './ui/views/activity-bar';
@@ -49,12 +49,12 @@ export async function activate(context: vscode.ExtensionContext) {
4949
logger.info('Cycode plugin is running');
5050

5151
const diagnosticCollection = vscode.languages.createDiagnosticCollection(extensionName);
52-
const treeView = createTreeView(context);
52+
const treeDataProvider = createTreeDataProvider(context);
5353

5454
const extension = container.resolve<IExtensionService>(ExtensionServiceSymbol);
5555
extension.extensionContext = context;
5656
extension.diagnosticCollection = diagnosticCollection;
57-
extension.treeView = treeView;
57+
extension.treeDataProvider = treeDataProvider;
5858

5959
// refactor this to porper class
6060
const extensionStatusBar = statusBar.create();

src/providers/tree-data/index.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,18 @@
11
import * as vscode from 'vscode';
22
import { TreeDataProvider } from './provider';
3-
import { BaseNode } from './nodes/base-node';
43

5-
export interface TreeView {
6-
provider: TreeDataProvider;
7-
view: vscode.TreeView<BaseNode>;
8-
}
9-
10-
export const createTreeView = (context: vscode.ExtensionContext): TreeView => {
11-
const provider = new TreeDataProvider();
12-
const view = vscode.window.createTreeView(TreeDataProvider.viewType, {
13-
treeDataProvider: provider,
14-
showCollapseAll: true,
15-
canSelectMany: false,
16-
});
4+
export const createTreeDataProvider = (context: vscode.ExtensionContext): TreeDataProvider => {
5+
const treeDataProvider = new TreeDataProvider();
6+
treeDataProvider.treeView = vscode.window.createTreeView(
7+
TreeDataProvider.viewType, { treeDataProvider: treeDataProvider },
8+
);
179

1810
context.subscriptions.push(
1911
vscode.window.registerTreeDataProvider(
2012
TreeDataProvider.viewType,
21-
provider,
13+
treeDataProvider,
2214
),
2315
);
24-
return { view, provider };
16+
17+
return treeDataProvider;
2518
};

src/providers/tree-data/nodes/detection-node.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export class DetectionNode extends BaseNode {
1717

1818
this.command = {
1919
title: '',
20-
command: VscodeCommands.OnTreeItemClick,
20+
command: VscodeCommands.OnTreeViewDetectionNodeClickCommand,
2121
arguments: [scanType, detection],
2222
};
2323
this.collapsibleState = vscode.TreeItemCollapsibleState.None;

src/providers/tree-data/nodes/scan-type-node.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ export class ScanTypeNode extends BaseNode {
1313

1414
this.scanType = scanType;
1515
this.contextValue = `${scanType.toLowerCase()}ScanTypeNode`;
16-
console.log(this.contextValue);
1716
}
1817
}

src/providers/tree-data/provider.ts

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,29 @@ import { container } from 'tsyringe';
88
import { IScanResultsService } from '../../services/scan-results-service';
99
import { ScanResultsServiceSymbol } from '../../symbols';
1010
import { DetectionBase } from '../../cli/models/scan-result/detection-base';
11+
import { VscodeCommands } from '../../commands';
1112

1213
export class TreeDataProvider implements vscode.TreeDataProvider<BaseNode> {
1314
public static readonly viewType = 'cycode.view.tree';
1415

15-
private _onDidChangeTreeData:
16-
vscode.EventEmitter<BaseNode | undefined> = new vscode.EventEmitter<BaseNode | undefined>();
16+
public treeView: vscode.TreeView<BaseNode>;
1717

18-
readonly onDidChangeTreeData:
19-
vscode.Event<BaseNode | undefined> = this._onDidChangeTreeData.event;
18+
private _onDidChangeTreeData: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
19+
readonly onDidChangeTreeData: vscode.Event<void> = this._onDidChangeTreeData.event;
2020

2121
private _createdRootNodes: ScanTypeNode[] = [];
2222
private _createdNodesToChildren = new Map<BaseNode, BaseNode[]>();
23+
private _createdChildToParentNode = new Map<BaseNode, BaseNode>();
2324

2425
getTreeItem(element: BaseNode): vscode.TreeItem {
2526
return element;
2627
}
2728

29+
getParent(element: BaseNode): vscode.ProviderResult<BaseNode> {
30+
// must be implemented properly to be able to call this.treeView.reveal()
31+
return this._createdChildToParentNode.get(element);
32+
}
33+
2834
getChildren(
2935
element?: BaseNode,
3036
): Thenable<BaseNode[]> {
@@ -36,18 +42,14 @@ export class TreeDataProvider implements vscode.TreeDataProvider<BaseNode> {
3642
}
3743

3844
private getSeverityWeight(severity: string): number {
39-
switch (severity) {
40-
case 'critical':
41-
return 4;
42-
case 'high':
43-
return 3;
44-
case 'medium':
45-
return 2;
46-
case 'low':
47-
return 1;
48-
default:
49-
return 0;
50-
}
45+
const severityWeights: Record<string, number> = {
46+
critical: 4,
47+
high: 3,
48+
medium: 2,
49+
low: 1,
50+
};
51+
52+
return severityWeights[severity.toLowerCase()] || 0;
5153
}
5254

5355
private getScanTypeNodeSummary(sortedDetections: DetectionBase[]): string {
@@ -71,7 +73,7 @@ export class TreeDataProvider implements vscode.TreeDataProvider<BaseNode> {
7173
const detections = scanResultsService.getDetections(scanType);
7274

7375
const severitySortedDetections = detections.sort((a, b) => {
74-
return this.getSeverityWeight(b.severity.toLowerCase()) - this.getSeverityWeight(a.severity.toLowerCase());
76+
return this.getSeverityWeight(b.severity) - this.getSeverityWeight(a.severity);
7577
});
7678
const groupedByFilepathDetection = severitySortedDetections
7779
.reduce<Map<string, DetectionBase[]>>((acc, detection) => {
@@ -91,22 +93,58 @@ export class TreeDataProvider implements vscode.TreeDataProvider<BaseNode> {
9193
const fileNode = new FileNode(filepath, detections.length);
9294
this._createdNodesToChildren.get(scanTypeNode)?.push(fileNode);
9395
this._createdNodesToChildren.set(fileNode, []);
96+
this._createdChildToParentNode.set(fileNode, scanTypeNode);
97+
9498
for (const detection of detections) {
9599
const detectionNode = new DetectionNode(scanType, detection);
96100
this._createdNodesToChildren.get(fileNode)?.push(detectionNode);
101+
this._createdChildToParentNode.set(detectionNode, fileNode);
97102
}
98103
}
99104
}
100105

101106
public refresh(): void {
102107
this._createdRootNodes = [];
103108
this._createdNodesToChildren.clear();
109+
this._createdChildToParentNode.clear();
104110

105111
this.createNodes(CliScanType.Secret);
106112
this.createNodes(CliScanType.Sca);
107113
this.createNodes(CliScanType.Iac);
108114
this.createNodes(CliScanType.Sast);
109115

110-
this._onDidChangeTreeData.fire(undefined);
116+
this._onDidChangeTreeData.fire();
117+
}
118+
119+
public async expandAll() {
120+
/*
121+
* vscode api limits rendering of expanding.
122+
* Editing of collapsibleState field doesn't affect nodes that are visible already.
123+
* To bypass this limitation, we can use reveal method instead.
124+
* This method is limited to depth of 3; it is enough to our tree.
125+
* More of the context: https://github.com/microsoft/vscode/issues/131955
126+
*/
127+
128+
const revealOptions = { expand: 3, select: false, focus: false };
129+
const thenables: Thenable<void>[] = [];
130+
for (const rootNode of this._createdRootNodes) {
131+
thenables.push(this.treeView.reveal(rootNode, revealOptions));
132+
for (const fileNode of this._createdNodesToChildren.get(rootNode) || []) {
133+
thenables.push(this.treeView.reveal(fileNode, revealOptions));
134+
}
135+
}
136+
137+
await Promise.all(thenables);
138+
}
139+
140+
public async collapseAll() {
141+
/*
142+
* It works excellently and always re-renders the tree view,
143+
* even already visible elements,
144+
* but unfortunately, there is no build-in "ExpandAll" command.
145+
* That's why we use our own implementation.
146+
*/
147+
148+
await vscode.commands.executeCommand(VscodeCommands.WorkbenchTreeViewCollapseAll);
111149
}
112150
}

src/services/cli-service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ export class CliService implements ICliService {
240240
)
241241
.then((buttonPressed) => {
242242
if (buttonPressed === TrayNotificationTexts.OpenProblemsTab) {
243-
vscode.commands.executeCommand(VscodeCommands.ShowProblemsTab);
243+
vscode.commands.executeCommand(VscodeCommands.WorkbenchShowProblemsTab);
244244
}
245245
});
246246
} else if (onDemand) {

0 commit comments

Comments
 (0)