diff --git a/images/serverManager.svg b/images/serverManager.svg new file mode 100644 index 0000000..53e5be5 --- /dev/null +++ b/images/serverManager.svg @@ -0,0 +1,14 @@ + + + + Slice + Created with Sketch. + + + + + + + + + diff --git a/images/toolsContainer.svg b/images/toolsContainer.svg new file mode 100644 index 0000000..53e5be5 --- /dev/null +++ b/images/toolsContainer.svg @@ -0,0 +1,14 @@ + + + + Slice + Created with Sketch. + + + + + + + + + diff --git a/package.json b/package.json index b8288c3..214b5db 100644 --- a/package.json +++ b/package.json @@ -59,11 +59,32 @@ }, "main": "./out/extension", "activationEvents": [ + "onView:intersystems-community_servermanager", + "onCommand:intersystems-community.servermanager.addServer", "onCommand:intersystems-community.servermanager.storePassword", "onCommand:intersystems-community.servermanager.clearPassword", "onCommand:intersystems-community.servermanager.importServers" ], "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "intersystems-community_servermanager", + "title": "InterSystems Tools", + "icon": "images/toolsContainer.svg" + } + ] + }, + "views": { + "intersystems-community_servermanager": [ + { + "id": "intersystems-community_servermanager", + "name": "Server Manager", + "contextualTitle": "InterSystems Server Manager", + "icon": "images/serverManager.svg" + } + ] + }, "configuration": { "title": "InterSystems Server Manager", "properties": { @@ -187,10 +208,28 @@ } }, "commands": [ + { + "command": "intersystems-community.servermanager.addServer", + "category": "InterSystems Server Manager", + "title": "Add Server", + "icon": "$(add)" + }, + { + "command": "intersystems-community.servermanager.openManagementPortalExternal", + "category": "InterSystems Server Manager", + "title": "Open Management Portal in External Browser", + "icon": "$(link-external)" + }, + { + "command": "intersystems-community.servermanager.openManagementPortalInSimpleBrowser", + "category": "InterSystems Server Manager", + "title": "Open Management Portal in Simple Browser Tab" + }, { "command": "intersystems-community.servermanager.storePassword", "category": "InterSystems Server Manager", - "title": "Store Password in Keychain" + "title": "Store Password in Keychain", + "icon": "$(key)" }, { "command": "intersystems-community.servermanager.clearPassword", @@ -208,6 +247,42 @@ { "command": "intersystems-community.servermanager.importServers", "when": "isWindows" + }, + { + "command": "intersystems-community.servermanager.openManagementPortalExternal", + "when": "false" + }, + { + "command": "intersystems-community.servermanager.openManagementPortalInSimpleBrowser", + "when": "false" + } + ], + "view/title": [ + { + "command": "intersystems-community.servermanager.addServer", + "when": "view == intersystems-community_servermanager", + "group": "navigation" + }, + { + "command": "intersystems-community.servermanager.importServers", + "when": "view == intersystems-community_servermanager && isWindows" + } + ], + "view/item/context": [ + { + "command": "intersystems-community.servermanager.openManagementPortalExternal", + "when": "view == intersystems-community_servermanager && viewItem == server", + "group": "inline" + }, + { + "command": "intersystems-community.servermanager.storePassword", + "when": "view == intersystems-community_servermanager && viewItem == server", + "group": "password@10" + }, + { + "command": "intersystems-community.servermanager.clearPassword", + "when": "view == intersystems-community_servermanager && viewItem == server", + "group": "password@20" } ] } diff --git a/src/api/getPortalUriWithCredentials.ts b/src/api/getPortalUriWithCredentials.ts new file mode 100644 index 0000000..9355702 --- /dev/null +++ b/src/api/getPortalUriWithCredentials.ts @@ -0,0 +1,29 @@ +import * as vscode from 'vscode'; +import { Uri } from 'vscode'; +import { getServerSpec } from './getServerSpec'; + +export async function getPortalUriWithCredentials(name: string, scope?: vscode.ConfigurationScope): Promise { + return getServerSpec(name, scope).then((spec) => { + if (typeof spec !== 'undefined') { + const webServer = spec.webServer; + let queryString = ''; + + // At this point we don't know if the target is IRIS or Cache, so add credentials in both formats. + // Deliberately put password before username, otherwise it is visible in VS Code's confirmation dialog triggered target domain + // hasn't been set as trusted. Likewise, deliberately put IRIS* after Cache* + if (spec?.password) { + const passwordEncoded = encodeURIComponent(spec.password); + queryString += `&CachePassword=${passwordEncoded}&IRISPassword=${passwordEncoded}`; + } + if (spec?.username) { + const usernameEncoded = encodeURIComponent(spec.username); + queryString += `&CacheUsername=${usernameEncoded}&IRISUsername=${usernameEncoded}`; + } + + // Push the credentials offscreen + queryString = '_=' + ' '.padStart(500,' ') + queryString; + + return vscode.Uri.parse(`${webServer.scheme}://${webServer.host}:${webServer.port}${webServer.pathPrefix}/csp/sys/UtilHome.csp?${queryString}`, true); + } + }) +} diff --git a/src/api/getServerSpec.ts b/src/api/getServerSpec.ts index 52db7e7..0eecf89 100644 --- a/src/api/getServerSpec.ts +++ b/src/api/getServerSpec.ts @@ -49,7 +49,7 @@ export async function getServerSpec(name: string, scope?: vscode.ConfigurationSc // Obtain password from session cache or keychain unless trying to connect anonymously if (server.username && !server.password) { if (credentialCache[name] && credentialCache[name].username === server.username) { - server.password = credentialCache[name]; + server.password = credentialCache[name].password; } else { const keychain = new Keychain(name); const password = await keychain.getPassword().then(result => { diff --git a/src/commands/managePasswords.ts b/src/commands/managePasswords.ts index 17db41c..1ea5adc 100644 --- a/src/commands/managePasswords.ts +++ b/src/commands/managePasswords.ts @@ -2,9 +2,14 @@ import * as vscode from 'vscode'; import { extensionId } from '../extension'; import { Keychain } from '../keychain'; import { credentialCache } from '../api/getServerSpec'; +import { getServerNames } from '../api/getServerNames'; +import { ServerTreeItem } from '../ui/serverManagerView'; -export async function storePassword(): Promise { - const name = await commonPickServer({matchOnDetail: true}); +export async function storePassword(treeItem?: ServerTreeItem): Promise { + if (treeItem && !getServerNames().some((value) => value.name === treeItem?.label)) { + treeItem = undefined; + } + const name = treeItem?.label || await commonPickServer({matchOnDetail: true}); let reply = ''; if (name) { await vscode.window @@ -30,9 +35,12 @@ export async function storePassword(): Promise { return reply; } -export async function clearPassword(): Promise { +export async function clearPassword(treeItem?: ServerTreeItem): Promise { + if (treeItem && !getServerNames().some((value) => value.name === treeItem?.label)) { + treeItem = undefined; + } let reply = ''; - const name = await commonPickServer({matchOnDetail: true}); + const name = treeItem?.label || await commonPickServer({matchOnDetail: true}); if (name) { credentialCache[name] = undefined; const keychain = new Keychain(name); diff --git a/src/extension.ts b/src/extension.ts index 1a3100b..dde536d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,6 +7,9 @@ import { getServerNames } from './api/getServerNames'; import { getServerSpec } from './api/getServerSpec'; import { storePassword, clearPassword } from './commands/managePasswords'; import { importFromRegistry } from './commands/importFromRegistry'; +import { ServerManagerView, ServerTreeItem } from './ui/serverManagerView'; +import { addServer } from './api/addServer'; +import { getPortalUriWithCredentials } from './api/getPortalUriWithCredentials'; export interface ServerName { name: string, @@ -42,8 +45,39 @@ export function activate(context: vscode.ExtensionContext) { // Register the commands context.subscriptions.push( - vscode.commands.registerCommand(`${extensionId}.storePassword`, () => { - storePassword() + vscode.commands.registerCommand(`${extensionId}.addServer`, () => { + addServer(); + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand(`${extensionId}.openManagementPortalExternal`, (server?: ServerTreeItem) => { + if (server?.contextValue === 'server' && server.label) { + getPortalUriWithCredentials(server.label).then((uriWithCredentials) => { + if (uriWithCredentials) { + vscode.env.openExternal(uriWithCredentials); + } + }); + } + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand(`${extensionId}.openManagementPortalInSimpleBrowser`, (server?: ServerTreeItem) => { + if (server?.contextValue === 'server' && server.label) { + getPortalUriWithCredentials(server.label).then((uriWithCredentials) => { + if (uriWithCredentials) { + //vscode.commands.executeCommand('simpleBrowser.api.open', uriWithCredentials); + // + // It is essential to pass skipEncoding=true when converting the uri to a string, + // otherwise the encoding done within Simple Browser / webview causes double-encoding of the querystring. + vscode.commands.executeCommand('simpleBrowser.show', uriWithCredentials.toString(true)); + } + }); + } + }) + ); + context.subscriptions.push( + vscode.commands.registerCommand(`${extensionId}.storePassword`, (server?: ServerTreeItem) => { + storePassword(server) .then((name) => { if (name && name.length > 0) { _onDidChangePassword.fire(name); @@ -52,8 +86,8 @@ export function activate(context: vscode.ExtensionContext) { }) ); context.subscriptions.push( - vscode.commands.registerCommand(`${extensionId}.clearPassword`, () => { - clearPassword() + vscode.commands.registerCommand(`${extensionId}.clearPassword`, (server?: ServerTreeItem) => { + clearPassword(server) .then((name) => { if (name && name.length > 0) { _onDidChangePassword.fire(name); @@ -61,11 +95,14 @@ export function activate(context: vscode.ExtensionContext) { }); }) ); - context.subscriptions.push( - vscode.commands.registerCommand(`${extensionId}.importServers`, () => { - importFromRegistry(); - }) - ); + context.subscriptions.push( + vscode.commands.registerCommand(`${extensionId}.importServers`, () => { + importFromRegistry(); + }) + ); + + // Server Manager View + new ServerManagerView(context); let api = { async pickServer(scope?: vscode.ConfigurationScope, options: vscode.QuickPickOptions = {}): Promise { diff --git a/src/ui/serverManagerView.ts b/src/ui/serverManagerView.ts new file mode 100644 index 0000000..353e820 --- /dev/null +++ b/src/ui/serverManagerView.ts @@ -0,0 +1,111 @@ +import * as vscode from 'vscode'; +import { getServerNames } from '../api/getServerNames'; +import { ServerName } from '../extension'; + +export class ServerManagerView { + + constructor(context: vscode.ExtensionContext) { + const treeDataProvider = new SMNodeProvider(); + const view = vscode.window.createTreeView('intersystems-community_servermanager', { treeDataProvider, showCollapseAll: false }); + context.subscriptions.push(view); + } +} + +export class SMNodeProvider implements vscode.TreeDataProvider { + + private _onDidChangeTreeData: vscode.EventEmitter = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor() { + } + + refresh(): void { + this._onDidChangeTreeData.fire(); + } + + getTreeItem(element:SMTreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: SMTreeItem): SMTreeItem[] { + const children: SMTreeItem[] = []; + if (!element) { + children.push(new SMTreeItem({label: 'Current', codiconName: 'home'})); + children.push(new SMTreeItem({label: 'Starred', codiconName: 'star'})); + children.push(new SMTreeItem({label: 'Recent', codiconName: 'history'})); + children.push(new SMTreeItem({label: 'All : Ordered', tooltip: 'Sequenced as found in settings.json', codiconName: 'list-ordered', getChildren: getChildrenServers, params: {sorted: false}})); + children.push(new SMTreeItem({label: 'All : Sorted', tooltip: 'Alphabetic order', codiconName: 'triangle-down', getChildren: getChildrenServers, params: {sorted: true}})); + return children; + } + else{ + return element.getChildren() + } + } +} + +interface SMItem { + label: string, + contextValue?: string, + tooltip?: string, + description?: string, + codiconName?: string, + getChildren?: Function, + params?: any +} + +export class SMTreeItem extends vscode.TreeItem { + + private readonly _getChildren?: Function; + private readonly _params?: any; + + constructor(item: SMItem) { + const collapsibleState = item.getChildren ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None; + super(item.label, collapsibleState); + + this.contextValue = item.contextValue; + this.tooltip = item.tooltip; + this.description = item.description; + if (item.codiconName) { + this.iconPath = new vscode.ThemeIcon(item.codiconName); + } + this._getChildren = item.getChildren; + this._params = item.params; + } + + public getChildren(): SMTreeItem[] { + if (this._getChildren) { + return this._getChildren(this, this._params); + } + else { + return []; + } + } +} + +function getChildrenServers(element?: SMTreeItem, params?: any): SMTreeItem[] { + const children: SMTreeItem[] = []; + const getAllServers = (sorted?: boolean): ServerTreeItem[] => { + let serverNames = getServerNames(); + if (sorted) { + serverNames = serverNames.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); + } + return serverNames.map((serverName) => { + return new ServerTreeItem(serverName); + }) + } + + getAllServers(params.sorted).map((server) => children.push(server)); + return children; +} + +export class ServerTreeItem extends SMTreeItem { + + constructor( + serverName: ServerName, + ) { + super({label: serverName.name, tooltip: serverName.description, description: serverName.detail}); + this.command = {command: 'intersystems-community.servermanager.openManagementPortalInSimpleBrowser', title: 'Open Management Portal in Simple Browser Tab', arguments: [this]}; + } + iconPath = new vscode.ThemeIcon('server-environment'); + contextValue = 'server'; +}