diff --git a/media/refresh-dark.svg b/media/refresh-dark.svg
new file mode 100644
index 0000000..9196015
--- /dev/null
+++ b/media/refresh-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/refresh-light.svg b/media/refresh-light.svg
new file mode 100644
index 0000000..7ab5a08
--- /dev/null
+++ b/media/refresh-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/remove-dark.svg b/media/remove-dark.svg
new file mode 100644
index 0000000..6e5f94a
--- /dev/null
+++ b/media/remove-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/media/remove-light.svg b/media/remove-light.svg
new file mode 100644
index 0000000..8bdcb0d
--- /dev/null
+++ b/media/remove-light.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index a3d8d1c..98a44a4 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,22 @@
{
"command": "poiex.dispose",
"title": "Remove All Notes (DANGER)"
+ },
+ {
+ "command": "iacAudit.refreshProjectTree",
+ "title": "Refresh",
+ "icon": {
+ "light": "media/refresh-dark.svg",
+ "dark": "media/refresh-light.svg"
+ }
+ },
+ {
+ "command": "iacAudit.deleteTreeProject",
+ "title": "Delete project",
+ "icon": {
+ "light": "media/remove-dark.svg",
+ "dark": "media/remove-light.svg"
+ }
}
],
"configuration": {
@@ -161,6 +177,20 @@
"group": "inline@2",
"when": "commentController == poiex"
}
+ ],
+ "view/title": [
+ {
+ "command": "iacAudit.refreshProjectTree",
+ "when": "view == iacAudit && workspaceFolderCount > 0 && iacAudit.isProjectOpen == false",
+ "group": "navigation"
+ }
+ ],
+ "view/item/context": [
+ {
+ "command": "iacAudit.deleteTreeProject",
+ "when": "view == iacAudit && workspaceFolderCount > 0 && iacAudit.isProjectOpen == false",
+ "group": "inline"
+ }
]
},
"viewsContainers": {
@@ -288,4 +318,4 @@
"type": "git",
"url": "https://github.com/doyensec/poiex.git"
}
-}
+}
\ No newline at end of file
diff --git a/src/comments.ts b/src/comments.ts
index f986181..cec8b83 100644
--- a/src/comments.ts
+++ b/src/comments.ts
@@ -613,11 +613,11 @@ export class IaCComments {
}
}
- dispose() {
- this.disposables.forEach((disposable) => {
+ async dispose() {
+ for (const disposable of this.disposables) {
this.context.subscriptions.splice(this.context.subscriptions.indexOf(disposable), 1);
- disposable.dispose();
- });
+ await disposable.dispose();
+ }
this.disposed = true;
}
}
diff --git a/src/constants.ts b/src/constants.ts
index f93b510..3379711 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -12,6 +12,9 @@ export const DIAGNOSTICS_CODENAME = 'poiex';
export const IAC_POI_MESSAGE = "IaC Point Of Intersection:";
export const INFRAMAP_TIMEOUT_MS = 10 * 1000;
export const INFRAMAP_DOWNLOADED_STATENAME = "inframapDownloading";
+export const PROJECT_TREE_VIEW_TITLE = "PoiEx: Project List";
+export const PROJECT_TREE_VIEW_NO_PROJECTS_MESSAGE = "No projects found.";
+export const PROJECT_TREE_VIEW_DB_ERROR_MESSAGE = "Invalid DB configuration.";
export const FLAG_UNFLAGGED = 0;
export const FLAG_FALSE = 1;
diff --git a/src/extension.ts b/src/extension.ts
index 8fd8375..badeafb 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -13,6 +13,7 @@ import { LocalDB } from './db';
import { RemoteDB } from './remote';
import * as comments from './comments';
import { IaCEncryption } from './encryption';
+import * as tree from './tree';
let db: LocalDB;
let rdb: RemoteDB;
@@ -20,6 +21,7 @@ let mComments: comments.IaCComments;
let pdb: IaCProjectDir;
let projectDisposables: vscode.Disposable[] = [];
let projectClosing: boolean = false;
+let projectTreeView: tree.ProjectTreeViewManager;
export async function initLocalDb(dbDir: string, projectUuid: string) {
// Create sqlite3 database in storage directory
@@ -212,18 +214,57 @@ async function init1(context: vscode.ExtensionContext, iacPath: string) {
}));
// Register command to open an existing project
- context.subscriptions.push(vscode.commands.registerCommand(`${constants.EXT_NAME}.openProject`, async () => {
+ context.subscriptions.push(vscode.commands.registerCommand(`${constants.EXT_NAME}.openProject`, async (projectUuid: string | undefined = undefined) => {
console.log('[IaC Main] Open project button pressed');
if (!ensureDbOk()) {
console.log('[IaC Main] Remote database not configured, cannot open project');
return;
}
+ let cb2 = async (choice: string | undefined) => {
+ if (choice === undefined) {
+ return;
+ }
+ let projectUuid = choice;
+ if (choice.includes(' $ ')) {
+ projectUuid = choice.split(' $ ')[1];
+ }
+ let project = await pdb.getProject(projectUuid);
+ assert(project !== null, "Project not found in local database");
+ if (project === null) { return; };
+ if (project[3] === null || project[2] !== null) {
+ openProject(context, iacUri, projectUuid);
+ return;
+ }
+ // Ask for project secret
+ vscode.window.showInputBox({
+ placeHolder: 'Please enter project secret',
+ prompt: 'Please enter project secret',
+ password: true,
+ validateInput: (value: string) => {
+ if (value === undefined || value === '') {
+ return 'Project secret cannot be empty';
+ }
+ return undefined;
+ }
+ }).then((projectSecret) => {
+ if (projectSecret === undefined) {
+ return;
+ }
+ openProject(context, iacUri, projectUuid, projectSecret);
+ });
+ };
+
let cb = async () => {
console.log('[IaC Main] Open project ready, executing callback');
+ if (projectUuid !== undefined) {
+ cb2(projectUuid);
+ return;
+ }
// Show list of projects as quickpick
let projectList = (await pdb.listProjects()) as {}[];
+ projectTreeView.update(projectList);
let projectNames = projectList.map((project: any) => project.name + " $ " + project.uuid);
console.log('[IaC Main] Open project got list of projects');
if (projectNames.length === 0) {
@@ -233,36 +274,7 @@ async function init1(context: vscode.ExtensionContext, iacPath: string) {
vscode.window.showQuickPick(projectNames, {
placeHolder: 'Please select a project to open'
- }).then(async (choice) => {
- if (choice === undefined) {
- return;
- }
- let projectUuid = choice.split(' $ ')[1];
- let project = await pdb.getProject(projectUuid);
- assert(project !== null, "Project not found in local database");
- if (project === null) { return; };
- if (project[3] === null || project[2] !== null) {
- openProject(context, iacUri, projectUuid);
- return;
- }
- // Ask for project secret
- vscode.window.showInputBox({
- placeHolder: 'Please enter project secret',
- prompt: 'Please enter project secret',
- password: true,
- validateInput: (value: string) => {
- if (value === undefined || value === '') {
- return 'Project secret cannot be empty';
- }
- return undefined;
- }
- }).then((projectSecret) => {
- if (projectSecret === undefined) {
- return;
- }
- openProject(context, iacUri, projectUuid, projectSecret);
- });
- });
+ }).then(cb2);
};
if (rdb.settingsEnabled()) {
@@ -284,9 +296,24 @@ async function init1(context: vscode.ExtensionContext, iacPath: string) {
let projectUuid = project.uuid;
await pdb.removeProject(projectUuid, rdb);
}
+ projectTreeView.update();
}
});
- }));
+ }));
+
+ // Experimental tree view
+ if (ensureDbOk()) {
+ projectTreeView = new tree.ProjectTreeViewManager(context, pdb, rdb);
+ context.subscriptions.push(projectTreeView);
+ projectTreeView.show();
+ projectTreeView.update();
+ }
+ else {
+ projectTreeView = new tree.ProjectTreeViewManager(context, pdb, undefined);
+ context.subscriptions.push(projectTreeView);
+ projectTreeView.showDbError();
+ console.log('[IaC Main] Remote database not configured, cannot show project list');
+ }
}
async function openProject(context: vscode.ExtensionContext, storageUri: vscode.Uri, projectUuid: string, projectSecret: string | null = null) {
@@ -325,6 +352,9 @@ async function openProject(context: vscode.ExtensionContext, storageUri: vscode.
vscode.commands.executeCommand('setContext', 'iacAudit.isProjectCreator', true);
vscode.commands.executeCommand('setContext', 'iacAudit.isProjectEncrypted', projectSecret !== null);
+ // Hide project tree view
+ projectTreeView.hide();
+
rdb.setProjectUuid(projectUuid, projectSecret);
await initLocalDb(storageUri.fsPath, projectUuid);
@@ -415,8 +445,8 @@ async function closeProject(context: vscode.ExtensionContext, projectUuid: strin
}
projectClosing = true;
+ await mComments.dispose();
mIacWebviewManager.dispose();
- mComments.dispose();
mIaCDiagnostics.dispose();
// Dispose all project disposables
@@ -439,6 +469,11 @@ async function closeProject(context: vscode.ExtensionContext, projectUuid: strin
vscode.commands.executeCommand('setContext', 'iacAudit.isProjectCreator', false);
vscode.commands.executeCommand('setContext', 'iacAudit.isProjectEncrypted', false);
+ // Show project tree view
+ projectTreeView.show().then(() => {
+ projectTreeView.update();
+ });
+
// Race condition prevention
projectClosing = false;
}
diff --git a/src/tree.ts b/src/tree.ts
new file mode 100644
index 0000000..9f28921
--- /dev/null
+++ b/src/tree.ts
@@ -0,0 +1,203 @@
+import * as vscode from 'vscode';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as util from 'util';
+
+import * as projects from './projects';
+import * as constants from './constants';
+import * as remote from './remote';
+
+export class ProjectTreeViewManager {
+ private context: vscode.ExtensionContext;
+ private pdb: projects.IaCProjectDir;
+ private rdb: remote.RemoteDB | undefined;
+ private projectTreeView: vscode.TreeView | undefined = undefined;
+ private projectTreeDataProvider: ProjectSelectorProvider | undefined = undefined;
+ private originalViewTitle: string | undefined = undefined;
+ private _disposables: vscode.Disposable[] = [];
+ private hideProgressBar: any = undefined;
+
+ constructor(context: vscode.ExtensionContext, pbd: projects.IaCProjectDir, rdb: remote.RemoteDB | undefined) {
+ this.context = context;
+ this.pdb = pbd;
+ this.rdb = rdb;
+
+ this._disposables.push(vscode.commands.registerCommand('iacAudit.refreshProjectTree', async () => {
+ if (this.hideProgressBar !== undefined) {
+ console.log("[IaC Tree] Already refreshing project list");
+ return;
+ }
+
+ vscode.window.withProgress({
+ location: { viewId: 'iacAudit' }
+ }, (progress, token) => {
+ return new Promise((resolve, reject) => {
+ this.hideProgressBar = resolve;
+ });
+ });
+
+ try {
+ await this.update();
+ }
+ catch (err: any) {
+ console.error(`[IaC Tree] Error refreshing project list: ${err}`);
+ }
+ finally {
+ if (this.hideProgressBar !== undefined) {
+ this.hideProgressBar();
+ this.hideProgressBar = undefined;
+ }
+ }
+ }));
+
+ this._disposables.push(vscode.commands.registerCommand('iacAudit.deleteTreeProject', async (project: ProjectItem) => {
+ console.log(`[IaC Tree] Deleting project ${project.uuid}`);
+ await this.pdb.removeProject(project.uuid, rdb);
+ await this.update();
+ }));
+ }
+
+ async show() {
+ this.projectTreeDataProvider = new ProjectSelectorProvider([]);
+ this.projectTreeView = vscode.window.createTreeView('iacAudit', {
+ treeDataProvider: this.projectTreeDataProvider,
+ });
+ this.originalViewTitle = this.projectTreeView.title;
+ this.projectTreeView.title = constants.PROJECT_TREE_VIEW_TITLE;
+ this.projectTreeView.message = undefined;
+ this.projectTreeView.onDidChangeSelection(async (e) => {
+ if (e.selection.length !== 1) {
+ console.log(`[IaC Tree] Invalid selection length: ${e.selection}`);
+ return;
+ }
+ let project = e.selection[0];
+ if (project instanceof ProjectItem) {
+ console.log(`[IaC Tree] Opening project ${project.uuid}`);
+ await vscode.commands.executeCommand(`${constants.EXT_NAME}.openProject`, project.uuid);
+ return;
+ }
+ console.log(`[IaC Tree] Invalid selection: ${e.selection}`);
+ });
+ }
+
+ async hide() {
+ if (this.projectTreeView === undefined) {
+ return;
+ }
+ this.projectTreeView.title = this.originalViewTitle || "";
+ this.projectTreeView.message = undefined;
+ await this.projectTreeView?.dispose();
+ }
+
+ async showDbError() {
+ if (this.projectTreeView === undefined) {
+ await this.show();
+ }
+ (this.projectTreeView as vscode.TreeView).message = constants.PROJECT_TREE_VIEW_DB_ERROR_MESSAGE;
+ }
+
+ private async syncRemoteDB(): Promise {
+ if (this.rdb === undefined) {
+ return false;
+ }
+
+ if (this.rdb.settingsEnabled()) {
+ // Promisify onDbReadyOnce
+ let onDbReadyOnce = (): Promise => {
+ let rrdb = this.rdb;
+ return new Promise(function(resolve, reject) {
+ if (rrdb === undefined) {
+ resolve();
+ }
+ else {
+ rrdb.onDbReadyOnce(resolve);
+ }
+ });
+ }
+
+ try {
+ await onDbReadyOnce();
+ await this.pdb.safeSyncProjects(this.rdb);
+ }
+ catch (err) {
+ console.error(`[IaC Tree] Error syncing remote DB: ${err}`);
+ return false;
+ }
+ return true;
+ }
+ else {
+ return false;
+ }
+ }
+
+ async update(projectList: {}[] | null = null) {
+ if (projectList == null) {
+ await this.syncRemoteDB();
+ projectList = (await this.pdb.listProjects()) as {}[];
+ }
+ if (this.projectTreeView !== undefined) {
+ this.projectTreeDataProvider?.update(projectList);
+ if (projectList.length === 0) {
+ this.projectTreeView.message = constants.PROJECT_TREE_VIEW_NO_PROJECTS_MESSAGE;
+ }
+ }
+ }
+
+ async dispose() {
+ await this.hide();
+
+ this._disposables.forEach((disposable) => {
+ disposable.dispose();
+ });
+ }
+}
+
+class ProjectSelectorProvider implements vscode.TreeDataProvider {
+ private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter();
+ readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event;
+
+ constructor(private projectList: any[]) { }
+
+ update(projectList: any[]) {
+ this.projectList = projectList;
+ this._onDidChangeTreeData.fire();
+ }
+
+ getTreeItem(element: ProjectItem): vscode.TreeItem {
+ return element;
+ }
+
+ getChildren(element?: ProjectItem): Thenable {
+ if (element) {
+ return Promise.resolve([]);
+ } else {
+ return Promise.resolve(this.getProjects());
+ }
+ }
+
+ private getProjects(): ProjectItem[] {
+ return this.projectList.map(
+ (project: any) =>
+ new ProjectItem(project.uuid, project.name, vscode.TreeItemCollapsibleState.None)
+ );
+ }
+}
+
+class ProjectItem extends vscode.TreeItem {
+ constructor(
+ public uuid: string,
+ private pname: string,
+ public readonly collapsibleState: vscode.TreeItemCollapsibleState
+ ) {
+ const label = pname;
+ super(label, collapsibleState);
+ this.tooltip = `${this.label} $ ${this.uuid}`;
+ this.description = this.uuid;
+ }
+
+ // TODO: add icons for projects
+ //iconPath = {
+ // light: path.join(__filename, '..', '..', 'resources', 'light', 'dependency.svg'),
+ // dark: path.join(__filename, '..', '..', 'resources', 'dark', 'dependency.svg')
+ //};
+}
\ No newline at end of file