diff --git a/package.json b/package.json index 6ee4651..acacee0 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,16 @@ "command": "mysql.selectTop1000", "title": "Select Top 1000", "category": "MySQL" + }, + { + "command": "mysql.editConnection", + "title": "Edit Connection", + "category": "MySQL" + }, + { + "command": "mysql.dropTable", + "title": "Drop table", + "category": "MySQL" } ], "menus": { @@ -100,7 +110,7 @@ { "command": "mysql.deleteConnection", "when": "view == mysql && viewItem == connection", - "group": "mysql@2" + "group": "mysql@3" }, { "command": "mysql.newQuery", @@ -121,6 +131,16 @@ "command": "mysql.refresh", "when": "view == mysql && viewItem == table", "group": "mysql@1" + }, + { + "command": "mysql.editConnection", + "when": "view == mysql && viewItem == connection", + "group": "mysql@2" + }, + { + "command": "mysql.dropTable", + "when": "view == mysql && viewItem == table", + "group": "mysql@2" } ] }, diff --git a/src/common/utility.ts b/src/common/utility.ts index 995f363..9a9ccf7 100644 --- a/src/common/utility.ts +++ b/src/common/utility.ts @@ -2,9 +2,13 @@ import * as asciitable from "asciitable"; import * as fs from "fs"; import * as mysql from "mysql"; +import * as uuidv1 from "uuid/v1"; import * as vscode from "vscode"; import { IConnection } from "../model/connection"; +import { ConnectionNode } from "../model/connectionNode"; +import { MySQLTreeDataProvider } from "../mysqlTreeDataProvider"; import { AppInsightsClient } from "./appInsightsClient"; +import { Constants } from "./constants"; import { Global } from "./global"; import { OutputChannel } from "./outputChannel"; @@ -47,7 +51,7 @@ export class Utility { sql = sql ? sql : vscode.window.activeTextEditor.document.getText(); connectionOptions = connectionOptions ? connectionOptions : Global.activeConnection; connectionOptions.multipleStatements = true; - const connection = Utility.createConnection(connectionOptions); + const connection = Utility.createMySQLConnection(connectionOptions); OutputChannel.appendLine("[Start] Executing MySQL query..."); connection.query(sql, (err, rows) => { @@ -82,7 +86,7 @@ export class Utility { return vscode.window.showTextDocument(textDocument); } - public static createConnection(connectionOptions: IConnection): any { + public static createMySQLConnection(connectionOptions: IConnection): any { const newConnectionOptions: any = Object.assign({}, connectionOptions); if (connectionOptions.certPath && fs.existsSync(connectionOptions.certPath)) { newConnectionOptions.ssl = { @@ -92,6 +96,78 @@ export class Utility { return mysql.createConnection(newConnectionOptions); } + public static async editConnection(connectionNode: ConnectionNode, context: vscode.ExtensionContext, mysqlTreeDataProvider: MySQLTreeDataProvider) { + if (connectionNode) { + connectionNode.editConnection(context, mysqlTreeDataProvider); + } else { + const selectedConnection = await Utility.pickConnection(context); + if (selectedConnection !== undefined) { + const dummyNode = new ConnectionNode(selectedConnection.id, selectedConnection.host, selectedConnection.user, selectedConnection.password, + selectedConnection.port, selectedConnection.certPath); + + dummyNode.editConnection(context, mysqlTreeDataProvider); + } + } + } + + public static async deleteConnection(connectionNode: ConnectionNode, context: vscode.ExtensionContext, mysqlTreeDataProvider: MySQLTreeDataProvider) { + if (connectionNode) { + connectionNode.deleteConnection(context, mysqlTreeDataProvider); + } else { + const selectedConnection = await Utility.pickConnection(context); + if (selectedConnection !== undefined) { + const dummyNode = new ConnectionNode(selectedConnection.id, selectedConnection.host, selectedConnection.user, selectedConnection.password, + selectedConnection.port, selectedConnection.certPath); + + dummyNode.deleteConnection(context, mysqlTreeDataProvider); + } + } + } + + public static async createConnectionFromInput(context: vscode.ExtensionContext, copyFrom?: IConnection): Promise { + const host = await vscode.window.showInputBox({ prompt: "The hostname of the database", placeHolder: "host", ignoreFocusOut: true, value: copyFrom !== undefined ? copyFrom.host : ""}); + if (!host) { + return; + } + + const user = await vscode.window.showInputBox({ prompt: "The MySQL user to authenticate as", placeHolder: "user", ignoreFocusOut: true, value: copyFrom !== undefined ? copyFrom.user : "" }); + if (!user) { + return; + } + + const pw = copyFrom !== undefined ? await Global.keytar.getPassword(Constants.ExtensionId, copyFrom.id) : ""; + const password = await vscode.window.showInputBox({ prompt: "The password of the MySQL user", placeHolder: "password", + ignoreFocusOut: true, password: true, + value: pw }); + if (password === undefined) { + return; + } + + const port = await vscode.window.showInputBox({ prompt: "The port number to connect to", placeHolder: "port", ignoreFocusOut: true, value: copyFrom !== undefined ? copyFrom.port : "" }); + if (!port) { + return; + } + + const certPath = await vscode.window.showInputBox({ prompt: "[Optional] SSL certificate path. Leave empty to ignore", placeHolder: "certificate file path", ignoreFocusOut: true, + value: copyFrom !== undefined ? copyFrom.certPath : "" }); + if (certPath === undefined) { + return; + } + + const connection: IConnection = { + host, + user, + password, + port, + certPath, + }; + + const id = copyFrom ? copyFrom.id : uuidv1(); + connection.id = id; + + return connection; + } + private static async hasActiveConnection(): Promise { let count = 5; while (!Global.activeConnection && count > 0) { @@ -106,4 +182,32 @@ export class Utility { setTimeout(resolve, ms); }); } + + private static async pickConnection(context: vscode.ExtensionContext): Promise { + let selectedConnection; + + const connections = context.globalState.get<{ [key: string]: IConnection }>(Constants.GlobalStateMySQLConectionsKey); + const items: vscode.QuickPickItem[] = []; + + if (connections) { + for (const id of Object.keys(connections)) { + const item = connections[id]; + items.push({ + label: item.host, + description: item.user}); + } + } + + await vscode.window.showQuickPick(items).then(async (selection) => { + // the user canceled the selection + if (!selection) { + return selectedConnection; + } + + const selectedConnectionId = Object.keys(connections)[items.indexOf(selection)]; + selectedConnection = connections[selectedConnectionId]; + selectedConnection.id = selectedConnectionId; + }); + return selectedConnection; + } } diff --git a/src/extension.ts b/src/extension.ts index 1410ff6..e3121a0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -24,7 +24,7 @@ export function activate(context: vscode.ExtensionContext) { })); context.subscriptions.push(vscode.commands.registerCommand("mysql.deleteConnection", (connectionNode: ConnectionNode) => { - connectionNode.deleteConnection(context, mysqlTreeDataProvider); + Utility.deleteConnection(connectionNode, context, mysqlTreeDataProvider); })); context.subscriptions.push(vscode.commands.registerCommand("mysql.runQuery", () => { @@ -38,6 +38,14 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(vscode.commands.registerCommand("mysql.selectTop1000", (tableNode: TableNode) => { tableNode.selectTop1000(); })); + + context.subscriptions.push(vscode.commands.registerCommand("mysql.editConnection", (connectionNode: ConnectionNode) => { + Utility.editConnection(connectionNode, context, mysqlTreeDataProvider); + })); + + context.subscriptions.push(vscode.commands.registerCommand("mysql.dropTable", (tableNode: TableNode) => { + tableNode.dropTable(); + })); } export function deactivate() { diff --git a/src/model/connection.ts b/src/model/connection.ts index 04a0941..5821140 100644 --- a/src/model/connection.ts +++ b/src/model/connection.ts @@ -1,4 +1,5 @@ export interface IConnection { + id?: string; readonly host: string; readonly user: string; readonly password?: string; diff --git a/src/model/connectionNode.ts b/src/model/connectionNode.ts index bdbf0b0..b143689 100644 --- a/src/model/connectionNode.ts +++ b/src/model/connectionNode.ts @@ -28,7 +28,7 @@ export class ConnectionNode implements INode { } public async getChildren(): Promise { - const connection = Utility.createConnection({ + const connection = Utility.createMySQLConnection({ host: this.host, user: this.user, password: this.password, @@ -61,6 +61,15 @@ export class ConnectionNode implements INode { } public async deleteConnection(context: vscode.ExtensionContext, mysqlTreeDataProvider: MySQLTreeDataProvider) { + const options: vscode.MessageOptions = { + modal: true, + }; + const answer = await vscode.window.showWarningMessage(`Are you sure you want to delete ${this.host}?`, options, "Delete connection"); + + if (answer === undefined) { + return; + } + AppInsightsClient.sendEvent("deleteConnection"); const connections = context.globalState.get<{ [key: string]: IConnection }>(Constants.GlobalStateMySQLConectionsKey); delete connections[this.id]; @@ -70,4 +79,37 @@ export class ConnectionNode implements INode { mysqlTreeDataProvider.refresh(); } + + public async editConnection(context: vscode.ExtensionContext, mysqlTreeDataProvider: MySQLTreeDataProvider) { + AppInsightsClient.sendEvent("editConncetion.start"); + + const copyFrom: IConnection = { + id: this.id, + host: this.host, + user: this.user, + password: this.password, + port: this.port, + certPath: this.certPath, + }; + const editConnection = await Utility.createConnectionFromInput(context, copyFrom); + + if (editConnection !== undefined) { + let connections = await context.globalState.get<{ [key: string]: IConnection }>(Constants.GlobalStateMySQLConectionsKey); + + if (!connections) { + connections = {}; + } + + connections[editConnection.id] = editConnection; + + if (editConnection.password) { + await Global.keytar.setPassword(Constants.ExtensionId, editConnection.id, editConnection.password); + } + + await context.globalState.update(Constants.GlobalStateMySQLConectionsKey, connections); + mysqlTreeDataProvider.refresh(); + } + + AppInsightsClient.sendEvent("editConncetion.end"); + } } diff --git a/src/model/databaseNode.ts b/src/model/databaseNode.ts index 94b5d1f..8e0fae5 100644 --- a/src/model/databaseNode.ts +++ b/src/model/databaseNode.ts @@ -25,7 +25,7 @@ export class DatabaseNode implements INode { } public async getChildren(): Promise { - const connection = Utility.createConnection({ + const connection = Utility.createMySQLConnection({ host: this.host, user: this.user, password: this.password, diff --git a/src/model/tableNode.ts b/src/model/tableNode.ts index b8ef1dc..d347b11 100644 --- a/src/model/tableNode.ts +++ b/src/model/tableNode.ts @@ -25,7 +25,7 @@ export class TableNode implements INode { } public async getChildren(): Promise { - const connection = Utility.createConnection({ + const connection = Utility.createMySQLConnection({ host: this.host, user: this.user, password: this.password, @@ -62,4 +62,31 @@ export class TableNode implements INode { Utility.runQuery(sql, connection); } + + public async dropTable() { + const options: vscode.MessageOptions = { + modal: true, + }; + const answer = await vscode.window.showWarningMessage(`Are you sure you want to drop table ${this.table}?`, options, "Drop table"); + + if (answer === undefined) { + return; + } + + AppInsightsClient.sendEvent("drop table"); + const sql = `DROP TABLE ${this.database}.${this.table}`; + Utility.createSQLTextDocument(sql); + + const connection = { + host: this.host, + user: this.user, + password: this.password, + port: this.port, + database: this.database, + certPath: this.certPath, + }; + Global.activeConnection = connection; + + Utility.runQuery(sql, connection); + } } diff --git a/src/mysqlTreeDataProvider.ts b/src/mysqlTreeDataProvider.ts index e001507..08d6506 100644 --- a/src/mysqlTreeDataProvider.ts +++ b/src/mysqlTreeDataProvider.ts @@ -1,9 +1,9 @@ import * as path from "path"; -import * as uuidv1 from "uuid/v1"; import * as vscode from "vscode"; import { AppInsightsClient } from "./common/appInsightsClient"; import { Constants } from "./common/constants"; import { Global } from "./common/global"; +import { Utility } from "./common/utility"; import { IConnection } from "./model/connection"; import { ConnectionNode } from "./model/connectionNode"; import { INode } from "./model/INode"; @@ -29,50 +29,24 @@ export class MySQLTreeDataProvider implements vscode.TreeDataProvider { public async addConnection() { AppInsightsClient.sendEvent("addConnection.start"); - const host = await vscode.window.showInputBox({ prompt: "The hostname of the database", placeHolder: "host", ignoreFocusOut: true }); - if (!host) { - return; - } - - const user = await vscode.window.showInputBox({ prompt: "The MySQL user to authenticate as", placeHolder: "user", ignoreFocusOut: true }); - if (!user) { - return; - } - const password = await vscode.window.showInputBox({ prompt: "The password of the MySQL user", placeHolder: "password", ignoreFocusOut: true, password: true }); - if (password === undefined) { - return; - } + const newConnection = await Utility.createConnectionFromInput(this.context); - const port = await vscode.window.showInputBox({ prompt: "The port number to connect to", placeHolder: "port", ignoreFocusOut: true, value: "3306" }); - if (!port) { - return; - } + if (newConnection !== undefined) { + let connections = await this.context.globalState.get<{ [key: string]: IConnection }>(Constants.GlobalStateMySQLConectionsKey); - const certPath = await vscode.window.showInputBox({ prompt: "[Optional] SSL certificate path. Leave empty to ignore", placeHolder: "certificate file path", ignoreFocusOut: true }); - if (certPath === undefined) { - return; - } - - let connections = this.context.globalState.get<{ [key: string]: IConnection }>(Constants.GlobalStateMySQLConectionsKey); - - if (!connections) { - connections = {}; - } + if (!connections) { + connections = {}; + } - const id = uuidv1(); - connections[id] = { - host, - user, - port, - certPath, - }; + connections[newConnection.id] = newConnection; - if (password) { - await Global.keytar.setPassword(Constants.ExtensionId, id, password); + if (newConnection.password) { + await Global.keytar.setPassword(Constants.ExtensionId, newConnection.id, newConnection.password); + } + await this.context.globalState.update(Constants.GlobalStateMySQLConectionsKey, connections); + this.refresh(); } - await this.context.globalState.update(Constants.GlobalStateMySQLConectionsKey, connections); - this.refresh(); AppInsightsClient.sendEvent("addConnection.end"); }