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 @@
+
+
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 @@
+
+
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';
+}